Selaa lähdekoodia

[release] v0.12.0-unstable43

Yann Stepienik 1 vuosi sitten
vanhempi
commit
050fe7484b
38 muutettua tiedostoa jossa 1062 lisäystä ja 1195 poistoa
  1. 1 1
      changelog.md
  2. 2 1
      client/src/api/downloadButton.jsx
  3. 10 0
      client/src/api/metrics.jsx
  4. 15 2
      client/src/index.jsx
  5. 5 0
      client/src/pages/config/routeConfigPage.jsx
  6. 5 3
      client/src/pages/config/users/formShortcuts.jsx
  7. 1 1
      client/src/pages/config/users/proxyman.jsx
  8. 0 121
      client/src/pages/dashboard/IncomeAreaChart.jsx
  9. 88 0
      client/src/pages/dashboard/MetricHeaders.jsx
  10. 0 85
      client/src/pages/dashboard/MonthlyBarChart.jsx
  11. 0 224
      client/src/pages/dashboard/OrdersTable.jsx
  12. 0 105
      client/src/pages/dashboard/ReportAreaChart.jsx
  13. 0 148
      client/src/pages/dashboard/SalesColumnChart.jsx
  14. 8 2
      client/src/pages/dashboard/components/plot.jsx
  15. 1 1
      client/src/pages/dashboard/components/table.jsx
  16. 0 66
      client/src/pages/dashboard/containerMetrics.jsx
  17. 208 0
      client/src/pages/dashboard/eventsExplorer.jsx
  18. 50 0
      client/src/pages/dashboard/eventsExplorerStandalone.jsx
  19. 4 320
      client/src/pages/dashboard/index.jsx
  20. 5 0
      client/src/pages/servapps/containers/index.jsx
  21. 2 2
      client/src/themes/palette.jsx
  22. 16 0
      client/src/themes/theme/index.jsx
  23. 195 31
      package-lock.json
  24. 4 1
      package.json
  25. 1 1
      src/CRON.go
  26. 20 1
      src/docker/docker.go
  27. 37 1
      src/docker/events.go
  28. 55 4
      src/httpServer.go
  29. 1 1
      src/metrics/aggl.go
  30. 72 0
      src/metrics/api.go
  31. 152 0
      src/metrics/events.go
  32. 0 59
      src/metrics/http.go
  33. 5 3
      src/metrics/index.go
  34. 43 0
      src/proxy/shield.go
  35. 24 2
      src/utils/db.go
  36. 31 0
      src/utils/events.go
  37. 1 8
      src/utils/middleware.go
  38. 0 1
      src/utils/utils.go

+ 1 - 1
changelog.md

@@ -7,7 +7,7 @@
  - Integrated a new docker-less mode of functioning for networking
  - Added Button to force reset HTTPS cert in settings
  - New color slider with reset buttons
- - Added a notification when updating a container
+ - Added a notification when updating a container, renewing certs, etc...
  - Improved icon loading speed, and added proper placeholder
  - Added lazyloading to URL and Servapp pages images
  - Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features

+ 2 - 1
client/src/api/downloadButton.jsx

@@ -2,7 +2,7 @@ import { ArrowDownOutlined } from "@ant-design/icons";
 import { Button } from "@mui/material";
 import ResponsiveButton from "../components/responseiveButton";
 
-export const DownloadFile = ({ filename, content, contentGetter, label }) => {
+export const DownloadFile = ({ filename, content, contentGetter, label, style }) => {
     const downloadFile = async () => {
         // Get the content
         if (contentGetter) {
@@ -39,6 +39,7 @@ export const DownloadFile = ({ filename, content, contentGetter, label }) => {
         <ResponsiveButton
             color="primary"
             onClick={downloadFile}
+            style={style}
             variant={"outlined"}
             startIcon={<ArrowDownOutlined />}
         >

+ 10 - 0
client/src/api/metrics.jsx

@@ -27,8 +27,18 @@ function list() {
   }))
 }
 
+function events(from, to, search = '', query = '', page = '', logLevel) {
+  return wrap(fetch('/cosmos/api/events?from=' + from + '&to=' + to + '&search=' + search + '&query=' + query + '&page=' + page + '&logLevel=' + logLevel, {
+    method: 'GET',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+  }))
+}
+
 export {
   get,
   reset,
   list,
+  events
 };

+ 15 - 2
client/src/index.jsx

@@ -1,6 +1,10 @@
 import { StrictMode } from 'react';
 import { createRoot } from 'react-dom/client';
 import { BrowserRouter } from 'react-router-dom';
+import customParseFormat from 'dayjs/plugin/customParseFormat'; // import this if you need to parse custom formats
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting
+import 'dayjs/locale/en-gb';
 
 // scroll bar
 import 'simplebar/src/simplebar.css';
@@ -17,6 +21,13 @@ import './index.css';
 import App from './App';
 import { store } from './store';
 import reportWebVitals from './reportWebVitals';
+import { LocalizationProvider } from '@mui/x-date-pickers';
+
+import dayjs from 'dayjs';
+import 'dayjs/locale/en-gb';
+dayjs.extend(customParseFormat); // if needed
+dayjs.extend(localizedFormat); // if needed
+dayjs.locale('en-gb');
 
 // ==============================|| MAIN - REACT DOM RENDER  ||============================== //
 
@@ -25,8 +36,10 @@ const root = createRoot(container); // createRoot(container!) if you use TypeScr
 root.render(
     <StrictMode>
         <ReduxProvider store={store}>
-            <BrowserRouter basename="/">
-                <App />
+            <BrowserRouter basename="/">    
+                <LocalizationProvider dateAdapter={AdapterDayjs}>
+                    <App />
+                </LocalizationProvider>
             </BrowserRouter>
         </ReduxProvider>
     </StrictMode>

+ 5 - 0
client/src/pages/config/routeConfigPage.jsx

@@ -9,6 +9,7 @@ import RouteSecurity from "./routes/routeSecurity";
 import RouteOverview from "./routes/routeoverview";
 import IsLoggedIn from "../../isLoggedIn";
 import RouteMetrics from "../dashboard/routeMonitoring";
+import EventExplorerStandalone from "../dashboard/eventsExplorerStandalone";
 
 const RouteConfigPage = () => {
   const { routeName } = useParams();
@@ -68,6 +69,10 @@ const RouteConfigPage = () => {
           title: 'Monitoring',
           children:  <RouteMetrics routeName={routeName} />
         },
+        {
+          title: 'Events',
+          children: <EventExplorerStandalone initLevel='info' initSearch={`{"object":"route@${routeName}"}`}/>
+        },
       ]}/>}
 
       {!config && <div style={{textAlign: 'center'}}>

+ 5 - 3
client/src/pages/config/users/formShortcuts.jsx

@@ -148,7 +148,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
   </Grid>
 }
 
-export const CosmosSelect = ({ name, onChange, label, formik, disabled, options }) => {
+export const CosmosSelect = ({ name, onChange, label, formik, disabled, options, style }) => {
   return <Grid item xs={12}>
     <Stack spacing={1}>
       <InputLabel htmlFor={name}>{label}</InputLabel>
@@ -171,6 +171,7 @@ export const CosmosSelect = ({ name, onChange, label, formik, disabled, options
         helperText={
           formik.touched[name] && formik.errors[name]
         }
+        style={style}
       >
         {options.map((option) => (
           <MenuItem key={option[0]} value={option[0]}>
@@ -212,8 +213,9 @@ export const CosmosCollapse = ({ children, title }) => {
             aria-controls="panel1a-content"
             id="panel1a-header"
           >
-            <Typography variant="h6">
-              {title}</Typography>
+            <Typography variant="h6" style={{width: '100%', marginRight: '20px'}}>
+              {title}
+            </Typography>
           </AccordionSummary>
           <AccordionDetails>
             {children}

+ 1 - 1
client/src/pages/config/users/proxyman.jsx

@@ -193,7 +193,7 @@ const ProxyManagement = () => {
             </>
           },
           { title: 'Network', screenMin: 'lg', clickable:false, field: (r) => 
-            <div style={{width: '450px', marginLeft: '-60px', marginBottom: '10px'}}>
+            <div style={{width: '400px', marginLeft: '-200px', marginBottom: '10px'}}>
               <MiniPlotComponent  metrics={[
                 "cosmos.proxy.route.bytes." + r.Name,
                 "cosmos.proxy.route.time." + r.Name,

+ 0 - 121
client/src/pages/dashboard/IncomeAreaChart.jsx

@@ -1,121 +0,0 @@
-import PropTypes from 'prop-types';
-import { useState, useEffect } from 'react';
-
-// material-ui
-import { useTheme } from '@mui/material/styles';
-
-// third-party
-import ReactApexChart from 'react-apexcharts';
-
-// chart options
-const areaChartOptions = {
-    chart: {
-        height: 450,
-        type: 'area',
-        toolbar: {
-            show: false
-        }
-    },
-    dataLabels: {
-        enabled: false
-    },
-    stroke: {
-        curve: 'smooth',
-        width: 2
-    },
-    grid: {
-        strokeDashArray: 0
-    }
-};
-
-// ==============================|| INCOME AREA CHART ||============================== //
-
-const IncomeAreaChart = ({ slot }) => {
-    const theme = useTheme();
-
-    const { primary, secondary } = theme.palette.text;
-    const line = theme.palette.divider;
-
-    const [options, setOptions] = useState(areaChartOptions);
-
-    useEffect(() => {
-        setOptions((prevState) => ({
-            ...prevState,
-            colors: [theme.palette.primary.main, theme.palette.primary[700]],
-            xaxis: {
-                categories:
-                    slot === 'month'
-                        ? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
-                        : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
-                labels: {
-                    style: {
-                        colors: [
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary,
-                            secondary
-                        ]
-                    }
-                },
-                axisBorder: {
-                    show: true,
-                    color: line
-                },
-                tickAmount: slot === 'month' ? 11 : 7
-            },
-            yaxis: {
-                labels: {
-                    style: {
-                        colors: [secondary]
-                    }
-                }
-            },
-            grid: {
-                borderColor: line
-            },
-            tooltip: {
-                theme: 'light'
-            }
-        }));
-    }, [primary, secondary, line, theme, slot]);
-
-    const [series, setSeries] = useState([
-        {
-            name: 'Page Views',
-            data: [0, 86, 28, 115, 48, 210, 136]
-        },
-        {
-            name: 'Sessions',
-            data: [0, 43, 14, 56, 24, 105, 68]
-        }
-    ]);
-
-    useEffect(() => {
-        setSeries([
-            {
-                name: 'Page Views',
-                data: slot === 'month' ? [76, 85, 101, 98, 87, 105, 91, 114, 94, 86, 115, 35] : [31, 40, 28, 51, 42, 109, 100]
-            },
-            {
-                name: 'Sessions',
-                data: slot === 'month' ? [110, 60, 150, 35, 60, 36, 26, 45, 65, 52, 53, 41] : [11, 32, 45, 32, 34, 52, 41]
-            }
-        ]);
-    }, [slot]);
-
-    return <ReactApexChart options={options} series={series} type="area" height={450} />;
-};
-
-IncomeAreaChart.propTypes = {
-    slot: PropTypes.string
-};
-
-export default IncomeAreaChart;

+ 88 - 0
client/src/pages/dashboard/MetricHeaders.jsx

@@ -0,0 +1,88 @@
+import { useEffect, useState } from 'react';
+
+// material-ui
+import {
+    Button,
+    Stack,
+} from '@mui/material';
+
+import { formatDate } from './components/utils';
+
+const MetricHeaders = ({loaded, slot, setSlot, zoom, setZoom}) => {
+    const resetZoom = () => {
+        setZoom({
+            xaxis: {}
+        });
+    }
+
+    let xAxis = [];
+
+    if(slot === 'latest') {
+      for(let i = 0; i < 100; i++) {
+        xAxis.unshift(i);
+      }
+    }
+    else if(slot === 'hourly') {
+      for(let i = 0; i < 48; i++) {
+        let now = new Date();
+        now.setHours(now.getHours() - i);
+        now.setMinutes(0);
+        now.setSeconds(0);
+        xAxis.unshift(formatDate(now, true));
+      }
+    } else if(slot === 'daily') {
+      for(let i = 0; i < 30; i++) {
+        let now = new Date();
+        now.setDate(now.getDate() - i);
+        xAxis.unshift(formatDate(now));
+      }
+    }
+
+    return (
+        <>
+        {loaded && <div style={{zIndex:2, position: 'relative'}}>
+        <Stack direction="row" alignItems="center" spacing={0} style={{marginTop: 10}}>
+            <Button
+                size="small"
+                onClick={() => {setSlot('latest'); resetZoom()}}
+                color={slot === 'latest' ? 'primary' : 'secondary'}
+                variant={slot === 'latest' ? 'outlined' : 'text'}
+            >
+                Latest
+            </Button>
+            <Button
+                size="small"
+                onClick={() => {setSlot('hourly'); resetZoom()}}
+                color={slot === 'hourly' ? 'primary' : 'secondary'}
+                variant={slot === 'hourly' ? 'outlined' : 'text'}
+            >
+                Hourly
+            </Button>
+            <Button
+                size="small"
+                onClick={() => {setSlot('daily'); resetZoom()}}
+                color={slot === 'daily' ? 'primary' : 'secondary'}
+                variant={slot === 'daily' ? 'outlined' : 'text'}
+            >
+                Daily
+            </Button>
+
+            {zoom.xaxis.min && <Button
+                size="small"
+                onClick={() => {
+                    setZoom({
+                        xaxis: {}
+                    });
+                }}
+                color={'primary'}
+                variant={'outlined'}
+            >
+                Reset Zoom
+            </Button>}
+        </Stack>
+        </div>}
+      </>
+    );
+};
+
+export default MetricHeaders;

+ 0 - 85
client/src/pages/dashboard/MonthlyBarChart.jsx

@@ -1,85 +0,0 @@
-import { useEffect, useState } from 'react';
-
-// material-ui
-import { useTheme } from '@mui/material/styles';
-
-// third-party
-import ReactApexChart from 'react-apexcharts';
-
-// chart options
-const barChartOptions = {
-    chart: {
-        type: 'bar',
-        height: 365,
-        toolbar: {
-            show: false
-        }
-    },
-    plotOptions: {
-        bar: {
-            columnWidth: '45%',
-            borderRadius: 4
-        }
-    },
-    dataLabels: {
-        enabled: false
-    },
-    xaxis: {
-        categories: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
-        axisBorder: {
-            show: false
-        },
-        axisTicks: {
-            show: false
-        }
-    },
-    yaxis: {
-        show: false
-    },
-    grid: {
-        show: false
-    }
-};
-
-// ==============================|| MONTHLY BAR CHART ||============================== //
-
-const MonthlyBarChart = () => {
-    const theme = useTheme();
-
-    const { primary, secondary } = theme.palette.text;
-    const info = theme.palette.info.light;
-
-    const [series] = useState([
-        {
-            data: [80, 95, 70, 42, 65, 55, 78]
-        }
-    ]);
-
-    const [options, setOptions] = useState(barChartOptions);
-
-    useEffect(() => {
-        setOptions((prevState) => ({
-            ...prevState,
-            colors: [info],
-            xaxis: {
-                labels: {
-                    style: {
-                        colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary]
-                    }
-                }
-            },
-            tooltip: {
-                theme: 'light'
-            }
-        }));
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [primary, info, secondary]);
-
-    return (
-        <div id="chart">
-            <ReactApexChart options={options} series={series} type="bar" height={365} />
-        </div>
-    );
-};
-
-export default MonthlyBarChart;

+ 0 - 224
client/src/pages/dashboard/OrdersTable.jsx

@@ -1,224 +0,0 @@
-import PropTypes from 'prop-types';
-import { useState } from 'react';
-import { Link as RouterLink } from 'react-router-dom';
-
-// material-ui
-import { Box, Link, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
-
-// third-party
-import NumberFormat from 'react-number-format';
-
-// project import
-import Dot from '../../components/@extended/Dot';
-
-function createData(trackingNo, name, fat, carbs, protein) {
-    return { trackingNo, name, fat, carbs, protein };
-}
-
-const rows = [
-    createData(84564564, 'Camera Lens', 40, 2, 40570),
-    createData(98764564, 'Laptop', 300, 0, 180139),
-    createData(98756325, 'Mobile', 355, 1, 90989),
-    createData(98652366, 'Handset', 50, 1, 10239),
-    createData(13286564, 'Computer Accessories', 100, 1, 83348),
-    createData(86739658, 'TV', 99, 0, 410780),
-    createData(13256498, 'Keyboard', 125, 2, 70999),
-    createData(98753263, 'Mouse', 89, 2, 10570),
-    createData(98753275, 'Desktop', 185, 1, 98063),
-    createData(98753291, 'Chair', 100, 0, 14001)
-];
-
-function descendingComparator(a, b, orderBy) {
-    if (b[orderBy] < a[orderBy]) {
-        return -1;
-    }
-    if (b[orderBy] > a[orderBy]) {
-        return 1;
-    }
-    return 0;
-}
-
-function getComparator(order, orderBy) {
-    return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy);
-}
-
-function stableSort(array, comparator) {
-    const stabilizedThis = array.map((el, index) => [el, index]);
-    stabilizedThis.sort((a, b) => {
-        const order = comparator(a[0], b[0]);
-        if (order !== 0) {
-            return order;
-        }
-        return a[1] - b[1];
-    });
-    return stabilizedThis.map((el) => el[0]);
-}
-
-// ==============================|| ORDER TABLE - HEADER CELL ||============================== //
-
-const headCells = [
-    {
-        id: 'trackingNo',
-        align: 'left',
-        disablePadding: false,
-        label: 'Tracking No.'
-    },
-    {
-        id: 'name',
-        align: 'left',
-        disablePadding: true,
-        label: 'Product Name'
-    },
-    {
-        id: 'fat',
-        align: 'right',
-        disablePadding: false,
-        label: 'Total Order'
-    },
-    {
-        id: 'carbs',
-        align: 'left',
-        disablePadding: false,
-
-        label: 'Status'
-    },
-    {
-        id: 'protein',
-        align: 'right',
-        disablePadding: false,
-        label: 'Total Amount'
-    }
-];
-
-// ==============================|| ORDER TABLE - HEADER ||============================== //
-
-function OrderTableHead({ order, orderBy }) {
-    return (
-        <TableHead>
-            <TableRow>
-                {headCells.map((headCell) => (
-                    <TableCell
-                        key={headCell.id}
-                        align={headCell.align}
-                        padding={headCell.disablePadding ? 'none' : 'normal'}
-                        sortDirection={orderBy === headCell.id ? order : false}
-                    >
-                        {headCell.label}
-                    </TableCell>
-                ))}
-            </TableRow>
-        </TableHead>
-    );
-}
-
-OrderTableHead.propTypes = {
-    order: PropTypes.string,
-    orderBy: PropTypes.string
-};
-
-// ==============================|| ORDER TABLE - STATUS ||============================== //
-
-const OrderStatus = ({ status }) => {
-    let color;
-    let title;
-
-    switch (status) {
-        case 0:
-            color = 'warning';
-            title = 'Pending';
-            break;
-        case 1:
-            color = 'success';
-            title = 'Approved';
-            break;
-        case 2:
-            color = 'error';
-            title = 'Rejected';
-            break;
-        default:
-            color = 'primary';
-            title = 'None';
-    }
-
-    return (
-        <Stack direction="row" spacing={1} alignItems="center">
-            <Dot color={color} />
-            <Typography>{title}</Typography>
-        </Stack>
-    );
-};
-
-OrderStatus.propTypes = {
-    status: PropTypes.number
-};
-
-// ==============================|| ORDER TABLE ||============================== //
-
-export default function OrderTable() {
-    const [order] = useState('asc');
-    const [orderBy] = useState('trackingNo');
-    const [selected] = useState([]);
-
-    const isSelected = (trackingNo) => selected.indexOf(trackingNo) !== -1;
-
-    return (
-        <Box>
-            <TableContainer
-                sx={{
-                    width: '100%',
-                    overflowX: 'auto',
-                    position: 'relative',
-                    display: 'block',
-                    maxWidth: '100%',
-                    '& td, & th': { whiteSpace: 'nowrap' }
-                }}
-            >
-                <Table
-                    aria-labelledby="tableTitle"
-                    sx={{
-                        '& .MuiTableCell-root:first-child': {
-                            pl: 2
-                        },
-                        '& .MuiTableCell-root:last-child': {
-                            pr: 3
-                        }
-                    }}
-                >
-                    <OrderTableHead order={order} orderBy={orderBy} />
-                    <TableBody>
-                        {stableSort(rows, getComparator(order, orderBy)).map((row, index) => {
-                            const isItemSelected = isSelected(row.trackingNo);
-                            const labelId = `enhanced-table-checkbox-${index}`;
-
-                            return (
-                                <TableRow
-                                    hover
-                                    role="checkbox"
-                                    sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
-                                    aria-checked={isItemSelected}
-                                    tabIndex={-1}
-                                    key={row.trackingNo}
-                                    selected={isItemSelected}
-                                >
-                                    <TableCell component="th" id={labelId} scope="row" align="left">
-                                        <Link color="secondary" component={RouterLink} to="">
-                                            {row.trackingNo}
-                                        </Link>
-                                    </TableCell>
-                                    <TableCell align="left">{row.name}</TableCell>
-                                    <TableCell align="right">{row.fat}</TableCell>
-                                    <TableCell align="left">
-                                        <OrderStatus status={row.carbs} />
-                                    </TableCell>
-                                    <TableCell align="right">
-                                        <NumberFormat value={row.protein} displayType="text" thousandSeparator prefix="$" />
-                                    </TableCell>
-                                </TableRow>
-                            );
-                        })}
-                    </TableBody>
-                </Table>
-            </TableContainer>
-        </Box>
-    );
-}

+ 0 - 105
client/src/pages/dashboard/ReportAreaChart.jsx

@@ -1,105 +0,0 @@
-import { useEffect, useState } from 'react';
-
-// material-ui
-import { useTheme } from '@mui/material/styles';
-
-// third-party
-import ReactApexChart from 'react-apexcharts';
-
-// chart options
-const areaChartOptions = {
-    chart: {
-        height: 340,
-        type: 'line',
-        toolbar: {
-            show: false
-        }
-    },
-    dataLabels: {
-        enabled: false
-    },
-    stroke: {
-        curve: 'smooth',
-        width: 1.5
-    },
-    grid: {
-        strokeDashArray: 4
-    },
-    xaxis: {
-        type: 'datetime',
-        categories: [
-            '2018-05-19T00:00:00.000Z',
-            '2018-06-19T00:00:00.000Z',
-            '2018-07-19T01:30:00.000Z',
-            '2018-08-19T02:30:00.000Z',
-            '2018-09-19T03:30:00.000Z',
-            '2018-10-19T04:30:00.000Z',
-            '2018-11-19T05:30:00.000Z',
-            '2018-12-19T06:30:00.000Z'
-        ],
-        labels: {
-            format: 'MMM'
-        },
-        axisBorder: {
-            show: false
-        },
-        axisTicks: {
-            show: false
-        }
-    },
-    yaxis: {
-        show: false
-    },
-    tooltip: {
-        x: {
-            format: 'MM'
-        }
-    }
-};
-
-// ==============================|| REPORT AREA CHART ||============================== //
-
-const ReportAreaChart = () => {
-    const theme = useTheme();
-
-    const { primary, secondary } = theme.palette.text;
-    const line = theme.palette.divider;
-
-    const [options, setOptions] = useState(areaChartOptions);
-
-    useEffect(() => {
-        setOptions((prevState) => ({
-            ...prevState,
-            colors: [theme.palette.warning.main],
-            xaxis: {
-                labels: {
-                    style: {
-                        colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary, secondary]
-                    }
-                }
-            },
-            grid: {
-                borderColor: line
-            },
-            tooltip: {
-                theme: 'light'
-            },
-            legend: {
-                labels: {
-                    colors: 'grey.500'
-                }
-            }
-        }));
-    }, [primary, secondary, line, theme]);
-
-    const [series] = useState([
-        {
-            name: 'Series 1',
-            data: [58, 115, 28, 83, 63, 75, 35, 55]
-        }
-    ]);
-
-    return <ReactApexChart options={options} series={series} type="line" height={345} />;
-};
-
-export default ReportAreaChart;

+ 0 - 148
client/src/pages/dashboard/SalesColumnChart.jsx

@@ -1,148 +0,0 @@
-import { useEffect, useState } from 'react';
-
-// material-ui
-import { useTheme } from '@mui/material/styles';
-
-// third-party
-import ReactApexChart from 'react-apexcharts';
-
-// chart options
-const columnChartOptions = {
-    chart: {
-        type: 'bar',
-        height: 430,
-        toolbar: {
-            show: false
-        }
-    },
-    plotOptions: {
-        bar: {
-            columnWidth: '30%',
-            borderRadius: 4
-        }
-    },
-    dataLabels: {
-        enabled: false
-    },
-    stroke: {
-        show: true,
-        width: 8,
-        colors: ['transparent']
-    },
-    xaxis: {
-        categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
-    },
-    yaxis: {
-        title: {
-            text: '$ (thousands)'
-        }
-    },
-    fill: {
-        opacity: 1
-    },
-    tooltip: {
-        y: {
-            formatter(val) {
-                return `$ ${val} thousands`;
-            }
-        }
-    },
-    legend: {
-        show: true,
-        fontFamily: `'Public Sans', sans-serif`,
-        offsetX: 10,
-        offsetY: 10,
-        labels: {
-            useSeriesColors: false
-        },
-        markers: {
-            width: 16,
-            height: 16,
-            radius: '50%',
-            offsexX: 2,
-            offsexY: 2
-        },
-        itemMargin: {
-            horizontal: 15,
-            vertical: 50
-        }
-    },
-    responsive: [
-        {
-            breakpoint: 600,
-            options: {
-                yaxis: {
-                    show: false
-                }
-            }
-        }
-    ]
-};
-
-// ==============================|| SALES COLUMN CHART ||============================== //
-
-const SalesColumnChart = () => {
-    const theme = useTheme();
-
-    const { primary, secondary } = theme.palette.text;
-    const line = theme.palette.divider;
-
-    const warning = theme.palette.warning.main;
-    const primaryMain = theme.palette.primary.main;
-    const successDark = theme.palette.success.dark;
-
-    const [series] = useState([
-        {
-            name: 'Net Profit',
-            data: [180, 90, 135, 114, 120, 145]
-        },
-        {
-            name: 'Revenue',
-            data: [120, 45, 78, 150, 168, 99]
-        }
-    ]);
-
-    const [options, setOptions] = useState(columnChartOptions);
-
-    useEffect(() => {
-        setOptions((prevState) => ({
-            ...prevState,
-            colors: [warning, primaryMain],
-            xaxis: {
-                labels: {
-                    style: {
-                        colors: [secondary, secondary, secondary, secondary, secondary, secondary]
-                    }
-                }
-            },
-            yaxis: {
-                labels: {
-                    style: {
-                        colors: [secondary]
-                    }
-                }
-            },
-            grid: {
-                borderColor: line
-            },
-            tooltip: {
-                theme: 'light'
-            },
-            legend: {
-                position: 'top',
-                horizontalAlign: 'right',
-                labels: {
-                    colors: 'grey.500'
-                }
-            }
-        }));
-    }, [primary, secondary, line, warning, primaryMain, successDark]);
-
-    return (
-        <div id="chart">
-            <ReactApexChart options={options} series={series} type="bar" height={430} />
-        </div>
-    );
-};
-
-export default SalesColumnChart;

+ 8 - 2
client/src/pages/dashboard/components/plot.jsx

@@ -73,8 +73,13 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z
         name: serie.Label,
         dataAxis: xAxis.map((date) => {
           if(slot === 'latest') {
-            return serie.Values[serie.Values.length - 1 - date] ? 
-              serie.Values[serie.Values.length - 1 - date].Value :
+            // let old = serie.Values.length - 1 - date;
+            // let realIndex = parseInt(serie.Values.length / xAxis.length  * date);
+            // let currentIndex = serie.Values.length - 1 - realIndex;
+            let index = serie.Values.length - 1 - parseInt(date / serie.TimeScale)
+            
+            return serie.Values[index] ? 
+              serie.Values[index].Value :
               0;
           } else {
             let key = slot === 'hourly' ? "hour_" : "day_";
@@ -129,6 +134,7 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z
           text: thisdata && thisdata.Label,
         },
         max: (thisdata && thisdata.Max) ? thisdata.Max : undefined,
+        min: (thisdata && thisdata.Max) ? 0 : undefined,
       })),
       grid: {
         borderColor: line

+ 1 - 1
client/src/pages/dashboard/components/table.jsx

@@ -173,7 +173,7 @@ const TableComponent = ({ title, data, displayMax, render, xAxis, slot, zoom}) =
               return 0;
             }
           } else {
-            let realIndex = item.Values.length - 1 - date
+            let realIndex = item.Values.length - 1 - parseInt(date / item.TimeScale)
             return item.Values[realIndex] ? item.Values[realIndex].Value : 0;
           }
         })

+ 0 - 66
client/src/pages/dashboard/containerMetrics.jsx

@@ -2,86 +2,21 @@ import { useEffect, useState } from 'react';
 
 // material-ui
 import {
-    Avatar,
-    AvatarGroup,
     Box,
     Button,
     Grid,
-    List,
-    ListItemAvatar,
-    ListItemButton,
-    ListItemSecondaryAction,
-    ListItemText,
-    MenuItem,
     Stack,
-    TextField,
     Typography,
-    Alert,
-    LinearProgress,
     CircularProgress
 } from '@mui/material';
 
-// project import
-import OrdersTable from './OrdersTable';
-import IncomeAreaChart from './IncomeAreaChart';
-import MonthlyBarChart from './MonthlyBarChart';
-import ReportAreaChart from './ReportAreaChart';
-import SalesColumnChart from './SalesColumnChart';
-import MainCard from '../../components/MainCard';
-import AnalyticEcommerce from '../../components/cards/statistics/AnalyticEcommerce';
-
-// assets
-import { GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
-import avatar1 from '../../assets/images/users/avatar-1.png';
-import avatar2 from '../../assets/images/users/avatar-2.png';
-import avatar3 from '../../assets/images/users/avatar-3.png';
-import avatar4 from '../../assets/images/users/avatar-4.png';
 import IsLoggedIn from '../../isLoggedIn';
 
 import * as API from '../../api';
-import AnimateButton from '../../components/@extended/AnimateButton';
 import PlotComponent from './components/plot';
-import TableComponent from './components/table';
-import { HomeBackground, TransparentHeader } from '../home';
 import { formatDate } from './components/utils';
 
-// avatar style
-const avatarSX = {
-    width: 36,
-    height: 36,
-    fontSize: '1rem'
-};
-
-// action style
-const actionSX = {
-    mt: 0.75,
-    ml: 1,
-    top: 'auto',
-    right: 'auto',
-    alignSelf: 'flex-start',
-    transform: 'none'
-};
-
-// sales report status
-const status = [
-    {
-        value: 'today',
-        label: 'Today'
-    },
-    {
-        value: 'month',
-        label: 'This Month'
-    },
-    {
-        value: 'year',
-        label: 'This Year'
-    }
-];
-
-// ==============================|| DASHBOARD - DEFAULT ||============================== //
-
 const ContainerMetrics = ({containerName}) => {
-    const [value, setValue] = useState('today');
     const [slot, setSlot] = useState('latest');
 
     const [zoom, setZoom] = useState({
@@ -90,7 +25,6 @@ const ContainerMetrics = ({containerName}) => {
 
     const [coStatus, setCoStatus] = useState(null);
     const [metrics, setMetrics] = useState(null);
-    const [isCreatingDB, setIsCreatingDB] = useState(false);
 
     const resetZoom = () => {
         setZoom({

+ 208 - 0
client/src/pages/dashboard/eventsExplorer.jsx

@@ -0,0 +1,208 @@
+import { useEffect, useState } from "react";
+import { toUTC } from "./components/utils";
+import * as API from '../../api';
+import { Button, CircularProgress, Stack, TextField } from "@mui/material";
+import { CosmosCollapse, CosmosSelect } from "../config/users/formShortcuts";
+import MainCard from '../../components/MainCard';
+import * as timeago from 'timeago.js';
+import { ExclamationOutlined, SettingOutlined } from "@ant-design/icons";
+import { Alert } from "@mui/material";
+import { DownloadFile } from "../../api/downloadButton";
+
+const EventsExplorer = ({from, to, xAxis, zoom, slot, initLevel, initSearch = ''}) => {
+	const [events, setEvents] = useState([]);
+	const [loading, setLoading] = useState(true);
+	const [search, setSearch] = useState(initSearch);
+	const [debouncedSearch, setDebouncedSearch] = useState(search);
+	const [total, setTotal] = useState(0);
+	const [remains, setRemains] = useState(0);
+	const [page, setPage] = useState(0);
+	const [logLevel, setLogLevel] = useState(initLevel || 'success');
+
+	if(typeof from != 'undefined' && typeof to != 'undefined') {
+		from = new Date(from);
+		to = new Date(to);
+	} else {
+		const zoomedXAxis = xAxis
+		.filter((date, index) => {
+			if (zoom && zoom.xaxis && zoom.xaxis.min && zoom.xaxis.max) {
+				return index >= zoom.xaxis.min && index <= zoom.xaxis.max;
+			}
+			return true;
+		})
+		.map((date) => {
+			if (slot === 'hourly' || slot === 'daily') {
+				let k = toUTC(date, slot === 'hourly');
+				return k;
+			} else {
+				let realIndex = xAxis.length - 1 - date
+				return realIndex;
+			}
+		})
+
+		const firstItem = zoomedXAxis[0];
+		const lastItem = zoomedXAxis[zoomedXAxis.length - 1];
+
+		if (slot === 'hourly' || slot === 'daily') {
+			from = new Date(firstItem);
+			to = new Date(lastItem);
+
+			if (slot === 'daily') {
+				to.setHours(23);
+				to.setMinutes(59);
+				to.setSeconds(59);
+				to.setMilliseconds(999);
+			} else {
+				to.setMinutes(to.getMinutes() + 59);
+				to.setSeconds(to.getSeconds() + 59);
+				to.setMilliseconds(to.getMilliseconds() + 999);
+			}
+		} else {
+			const now = new Date();
+			// round to 30 seconds
+			now.setSeconds(now.getSeconds() - now.getSeconds() % 30);
+			// remove microseconds
+			now.setMilliseconds(0);
+
+			from = new Date(now.getTime() - lastItem * 30000);
+			to = new Date(now.getTime() - firstItem * 30000);
+
+			// add 29 seconds to the end
+			to.setSeconds(to.getSeconds() + 29);
+		}
+	}
+
+	const refresh = (_page) => {
+		setLoading(true);
+		let _search = debouncedSearch;
+		let _query = "";
+		if (_search.startsWith('{') || _search.startsWith('[')) {
+			_search = ""
+			_query = debouncedSearch;
+		}
+		return API.metrics.events(from.toISOString(), to.toISOString(), _search, _query, _page, logLevel).then((res) => {
+			setEvents(res.data);
+			setLoading(false);
+			setTotal(res.total);
+			setRemains(res.total - res.data.length);
+		});
+	}
+
+	useEffect(() => {
+		setPage(0);
+		if (debouncedSearch.length === 0 || debouncedSearch.length > 2) {
+			refresh("");
+		}
+	}, [debouncedSearch, xAxis, zoom, slot, logLevel]);
+
+	useEffect(() => {
+		// Set a timeout to update debounced search after 1 second
+		const handler = setTimeout(() => {
+			setDebouncedSearch(search);
+		}, 500);
+	
+		// Clear the timeout if search changes before the 1 second has passed
+		return () => {
+			clearTimeout(handler);
+		};
+	}, [search]); // Only re-run if search changes
+
+	useEffect(() => {
+		setLoading(true);
+		refresh(page);
+	}, [page]);
+
+	return (<div>
+		<Stack spacing={2} direction="column" style={{width: '100%'}}>
+			<Stack spacing={2} direction="row" style={{width: '100%'}}>
+				<div>
+					<Button variant='contained' onClick={() => {
+						refresh("");
+					}} style={{height: '42px'}}>Refresh</Button>
+				</div>
+				<div>
+					<DownloadFile filename='events-export.json' content={
+						JSON.stringify(events, null, 2)
+					}  style={{height: '42px'}} label='export' />
+				</div>
+				<div>
+					<CosmosSelect
+						name={'level'}
+						formik={{
+							values: {
+								level: logLevel
+							},
+							touched: {},
+							errors: {},
+							setFieldValue: () => {},
+							handleChange: () => {}
+						}}
+						options={[
+							['debug', 'Debug'],
+							['info', 'Info'],
+							['success', 'Success'],
+							['warning', 'Warning'],
+							['important', 'Important'],
+							['error', 'Error'],
+						]}
+						onChange={(e) => {
+							setLogLevel(e.target.value);
+						}}
+						style={{
+							width: '100px',
+							margin:0,
+						}}
+					/>
+				</div>
+				<TextField fullWidth value={search} onChange={(e) => setSearch(e.target.value)} placeholder='Search (text or bson)' />
+			</Stack>
+			<div>
+			{total} events found from {from.toLocaleString()} to {to.toLocaleString()}
+			</div>
+			<div>
+				{events && <Stack spacing={1}>
+					{events.map((event) => {
+						return <div key={event.id} style={eventStyle(event)}>
+							<CosmosCollapse title={
+								<Alert severity={event.level} icon={
+									event.level == "debug" ? <SettingOutlined /> : event.level == "important" ? <ExclamationOutlined /> : undefined
+								}>
+									<div style={{fontWeight: 'bold', fontSize: '120%'}}>{event.label}</div>
+									<div>{(new Date(event.date)).toLocaleString()} - {timeago.format(event.date)}</div>
+									<div>{event.eventId} - {event.object}</div>
+								</Alert>}>
+								<div style={{overflow: 'auto'}}>
+										<pre style={{
+											whiteSpace: 'pre-wrap',
+											overflowX: 'auto',
+											maxWidth: '100%',
+											maxHeight: '400px',
+									}}>
+										{JSON.stringify(event, null, 2)}
+									</pre>
+								</div>
+							</CosmosCollapse>
+						</div>
+					})}
+					{loading && <div style={{textAlign: "center"}}>
+						<CircularProgress />
+					</div>}
+					{!loading && (remains > 0) && <MainCard>
+						<Button variant='contained' fullWidth onClick={() => {
+							// set page to last element's id
+							setPage(events[events.length - 1].id);
+						}}>Load more</Button>
+					</MainCard>}
+				</Stack>}
+			</div>
+		</Stack>
+		
+	</div>);
+}
+
+export default EventsExplorer;
+
+const eventStyle = (event) => ({
+	padding: 4,
+	borderRadius: 4,
+});

+ 50 - 0
client/src/pages/dashboard/eventsExplorerStandalone.jsx

@@ -0,0 +1,50 @@
+import { useEffect, useRef, useState } from 'react';
+import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting
+import 'dayjs/locale/en-gb';
+
+// material-ui
+import {
+    Grid,
+    Stack,
+    Typography,
+} from '@mui/material';
+
+
+import EventsExplorer from './eventsExplorer';
+import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
+
+import dayjs from 'dayjs';
+
+dayjs.extend(localizedFormat); // if needed
+dayjs.locale('en-gb');
+
+const EventExplorerStandalone = ({initSearch, initLevel}) => {
+  // one hour ago
+  const now = dayjs();
+  const [from, setFrom] = useState(now.subtract(1, 'hour'));
+  const [to, setTo] = useState(now)
+
+  return (
+      <>
+      <div style={{zIndex:2, position: 'relative'}}>
+          <Grid container rowSpacing={4.5} columnSpacing={2.75} >
+              <Grid item xs={12} sx={{ mb: -2.25 }}>
+                  <Typography variant="h4">Events</Typography>
+                  
+                  <Stack direction="row" spacing={2} sx={{ mt: 1.5 }}>
+                    <DateTimePicker label="From" value={from} onChange={(e) => setFrom(e)} />
+                    <DateTimePicker label="To" value={to} onChange={(e) => setTo(e)} />
+                  </Stack>
+              </Grid>
+
+
+              <Grid item xs={12} md={12} lg={12}>
+                <EventsExplorer initLevel={initLevel} initSearch={initSearch} from= {from} to= {to}/>
+              </Grid>
+          </Grid>
+      </div>
+    </>
+  );
+};
+
+export default EventExplorerStandalone;

+ 4 - 320
client/src/pages/dashboard/index.jsx

@@ -7,83 +7,22 @@ import {
     Box,
     Button,
     Grid,
-    List,
-    ListItemAvatar,
-    ListItemButton,
-    ListItemSecondaryAction,
-    ListItemText,
-    MenuItem,
-    Stack,
-    TextField,
     Typography,
     Alert,
     LinearProgress,
     CircularProgress
 } from '@mui/material';
 
-// project import
-import OrdersTable from './OrdersTable';
-import IncomeAreaChart from './IncomeAreaChart';
-import MonthlyBarChart from './MonthlyBarChart';
-import ReportAreaChart from './ReportAreaChart';
-import SalesColumnChart from './SalesColumnChart';
-import MainCard from '../../components/MainCard';
-import AnalyticEcommerce from '../../components/cards/statistics/AnalyticEcommerce';
-
-// assets
-import { GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
-import avatar1 from '../../assets/images/users/avatar-1.png';
-import avatar2 from '../../assets/images/users/avatar-2.png';
-import avatar3 from '../../assets/images/users/avatar-3.png';
-import avatar4 from '../../assets/images/users/avatar-4.png';
 import IsLoggedIn from '../../isLoggedIn';
 
 import * as API from '../../api';
-import AnimateButton from '../../components/@extended/AnimateButton';
-import PlotComponent from './components/plot';
-import TableComponent from './components/table';
-import { HomeBackground, TransparentHeader } from '../home';
 import { formatDate } from './components/utils';
-import MiniPlotComponent from './components/mini-plot';
 import ResourceDashboard from './resourceDashboard';
 import PrettyTabbedView from '../../components/tabbedView/tabbedView';
 import ProxyDashboard from './proxyDashboard';
 import AlertPage from './AlertPage';
-
-// avatar style
-const avatarSX = {
-    width: 36,
-    height: 36,
-    fontSize: '1rem'
-};
-
-// action style
-const actionSX = {
-    mt: 0.75,
-    ml: 1,
-    top: 'auto',
-    right: 'auto',
-    alignSelf: 'flex-start',
-    transform: 'none'
-};
-
-// sales report status
-const status = [
-    {
-        value: 'today',
-        label: 'Today'
-    },
-    {
-        value: 'month',
-        label: 'This Month'
-    },
-    {
-        value: 'year',
-        label: 'This Year'
-    }
-];
-
-// ==============================|| DASHBOARD - DEFAULT ||============================== //
+import EventsExplorer from './eventsExplorer';
+import MetricHeaders from './MetricHeaders';
 
 const DashboardDefault = () => {
     const [value, setValue] = useState('today');
@@ -179,8 +118,6 @@ const DashboardDefault = () => {
 
     return (
         <>
-        {/* <HomeBackground status={coStatus} />
-        <TransparentHeader /> */}
         <IsLoggedIn />
         {!metrics && <Box style={{
           width: '100%',
@@ -199,45 +136,7 @@ const DashboardDefault = () => {
             <Grid container rowSpacing={4.5} columnSpacing={2.75} >
                 <Grid item xs={12} sx={{ mb: -2.25 }}>
                     <Typography variant="h4">Server Monitoring</Typography>
-                    {currentTab <= 2 && <Stack direction="row" alignItems="center" spacing={0} style={{marginTop: 10}}>
-                        <Button
-                            size="small"
-                            onClick={() => {setSlot('latest'); resetZoom()}}
-                            color={slot === 'latest' ? 'primary' : 'secondary'}
-                            variant={slot === 'latest' ? 'outlined' : 'text'}
-                        >
-                            Latest
-                        </Button>
-                        <Button
-                            size="small"
-                            onClick={() => {setSlot('hourly'); resetZoom()}}
-                            color={slot === 'hourly' ? 'primary' : 'secondary'}
-                            variant={slot === 'hourly' ? 'outlined' : 'text'}
-                        >
-                            Hourly
-                        </Button>
-                        <Button
-                            size="small"
-                            onClick={() => {setSlot('daily'); resetZoom()}}
-                            color={slot === 'daily' ? 'primary' : 'secondary'}
-                            variant={slot === 'daily' ? 'outlined' : 'text'}
-                        >
-                            Daily
-                        </Button>
-
-                        {zoom.xaxis.min && <Button
-                            size="small"
-                            onClick={() => {
-                                setZoom({
-                                    xaxis: {}
-                                });
-                            }}
-                            color={'primary'}
-                            variant={'outlined'}
-                        >
-                            Reset Zoom
-                        </Button>}
-                    </Stack>}
+                    {currentTab <= 2 && <MetricHeaders loaded={metrics} slot={slot} setSlot={setSlot} zoom={zoom} setZoom={setZoom} />}
                     {currentTab > 2 && <div style={{height: 41}}></div>}
                 </Grid>
 
@@ -262,7 +161,7 @@ const DashboardDefault = () => {
                             },
                             {
                                 title: 'Events',
-                                children: <AlertPage />
+                                children: <EventsExplorer xAxis={xAxis} zoom={zoom} setZoom={setZoom} slot={slot} metrics={metrics} />
                             },
                             {
                                 title: 'Alerts',
@@ -271,221 +170,6 @@ const DashboardDefault = () => {
                         ]}
                     />
                 </Grid>
-
-
-                {/* 
-                <Grid item xs={12} sx={{ mb: -2.25 }}>
-                    <Typography variant="h5">Dashboard</Typography>
-                </Grid>
-                <Grid item xs={12} sm={6} md={4} lg={3}>
-                    <AnalyticEcommerce title="Total Page Views" count="4,42,236" percentage={59.3} extra="35,000" />
-                </Grid>
-                <Grid item xs={12} sm={6} md={4} lg={3}>
-                    <AnalyticEcommerce title="Total Users" count="78,250" percentage={70.5} extra="8,900" />
-                </Grid>
-                <Grid item xs={12} sm={6} md={4} lg={3}>
-                    <AnalyticEcommerce title="Total Order" count="18,800" percentage={27.4} isLoss color="warning" extra="1,943" />
-                </Grid>
-                <Grid item xs={12} sm={6} md={4} lg={3}>
-                    <AnalyticEcommerce title="Total Sales" count="$35,078" percentage={27.4} isLoss color="warning" extra="$20,395" />
-                </Grid>
-
-                <Grid item md={8} sx={{ display: { sm: 'none', md: 'block', lg: 'none' } }} />
-                */}
-                {/* 
-                <Grid item xs={12} md={7} lg={8}>
-                    <Grid container alignItems="center" justifyContent="space-between">
-                        <Grid item>
-                            <Typography variant="h5">Recent Orders</Typography>
-                        </Grid>
-                        <Grid item />
-                    </Grid>
-                    <MainCard sx={{ mt: 2 }} content={false}>
-                        <OrdersTable />
-                    </MainCard>
-                </Grid>
-
-                <Grid item xs={12} md={5} lg={4}>
-                    <Grid container alignItems="center" justifyContent="space-between">
-                        <Grid item>
-                            <Typography variant="h5">Analytics Report</Typography>
-                        </Grid>
-                        <Grid item />
-                    </Grid>
-                    <MainCard sx={{ mt: 2 }} content={false}>
-                        <List sx={{ p: 0, '& .MuiListItemButton-root': { py: 2 } }}>
-                            <ListItemButton divider>
-                                <ListItemText primary="Company Finance Growth" />
-                                <Typography variant="h5">+45.14%</Typography>
-                            </ListItemButton>
-                            <ListItemButton divider>
-                                <ListItemText primary="Company Expenses Ratio" />
-                                <Typography variant="h5">0.58%</Typography>
-                            </ListItemButton>
-                            <ListItemButton>
-                                <ListItemText primary="Business Risk Cases" />
-                                <Typography variant="h5">Low</Typography>
-                            </ListItemButton>
-                        </List>
-                        <ReportAreaChart />
-                    </MainCard>
-                </Grid>
-
-                <Grid item xs={12} md={7} lg={8}>
-                    <Grid container alignItems="center" justifyContent="space-between">
-                        <Grid item>
-                            <Typography variant="h5">Sales Report</Typography>
-                        </Grid>
-                        <Grid item>
-                            <TextField
-                                id="standard-select-currency"
-                                size="small"
-                                select
-                                value={value}
-                                onChange={(e) => setValue(e.target.value)}
-                                sx={{ '& .MuiInputBase-input': { py: 0.5, fontSize: '0.875rem' } }}
-                            >
-                                {status.map((option) => (
-                                    <MenuItem key={option.value} value={option.value}>
-                                        {option.label}
-                                    </MenuItem>
-                                ))}
-                            </TextField>
-                        </Grid>
-                    </Grid>
-                    <MainCard sx={{ mt: 1.75 }}>
-                        <Stack spacing={1.5} sx={{ mb: -12 }}>
-                            <Typography variant="h6" color="secondary">
-                                Net Profit
-                            </Typography>
-                            <Typography variant="h4">$1560</Typography>
-                        </Stack>
-                        <SalesColumnChart />
-                    </MainCard>
-                </Grid>
-                <Grid item xs={12} md={5} lg={4}>
-                    <Grid container alignItems="center" justifyContent="space-between">
-                        <Grid item>
-                            <Typography variant="h5">Transaction History</Typography>
-                        </Grid>
-                        <Grid item />
-                    </Grid>
-                    <MainCard sx={{ mt: 2 }} content={false}>
-                        <List
-                            component="nav"
-                            sx={{
-                                px: 0,
-                                py: 0,
-                                '& .MuiListItemButton-root': {
-                                    py: 1.5,
-                                    '& .MuiAvatar-root': avatarSX,
-                                    '& .MuiListItemSecondaryAction-root': { ...actionSX, position: 'relative' }
-                                }
-                            }}
-                        >
-                            <ListItemButton divider>
-                                <ListItemAvatar>
-                                    <Avatar
-                                        sx={{
-                                            color: 'success.main',
-                                            bgcolor: 'success.lighter'
-                                        }}
-                                    >
-                                        <GiftOutlined />
-                                    </Avatar>
-                                </ListItemAvatar>
-                                <ListItemText primary={<Typography variant="subtitle1">Order #002434</Typography>} secondary="Today, 2:00 AM" />
-                                <ListItemSecondaryAction>
-                                    <Stack alignItems="flex-end">
-                                        <Typography variant="subtitle1" noWrap>
-                                            + $1,430
-                                        </Typography>
-                                        <Typography variant="h6" color="secondary" noWrap>
-                                            78%
-                                        </Typography>
-                                    </Stack>
-                                </ListItemSecondaryAction>
-                            </ListItemButton>
-                            <ListItemButton divider>
-                                <ListItemAvatar>
-                                    <Avatar
-                                        sx={{
-                                            color: 'primary.main',
-                                            bgcolor: 'primary.lighter'
-                                        }}
-                                    >
-                                        <MessageOutlined />
-                                    </Avatar>
-                                </ListItemAvatar>
-                                <ListItemText
-                                    primary={<Typography variant="subtitle1">Order #984947</Typography>}
-                                    secondary="5 August, 1:45 PM"
-                                />
-                                <ListItemSecondaryAction>
-                                    <Stack alignItems="flex-end">
-                                        <Typography variant="subtitle1" noWrap>
-                                            + $302
-                                        </Typography>
-                                        <Typography variant="h6" color="secondary" noWrap>
-                                            8%
-                                        </Typography>
-                                    </Stack>
-                                </ListItemSecondaryAction>
-                            </ListItemButton>
-                            <ListItemButton>
-                                <ListItemAvatar>
-                                    <Avatar
-                                        sx={{
-                                            color: 'error.main',
-                                            bgcolor: 'error.lighter'
-                                        }}
-                                    >
-                                        <SettingOutlined />
-                                    </Avatar>
-                                </ListItemAvatar>
-                                <ListItemText primary={<Typography variant="subtitle1">Order #988784</Typography>} secondary="7 hours ago" />
-                                <ListItemSecondaryAction>
-                                    <Stack alignItems="flex-end">
-                                        <Typography variant="subtitle1" noWrap>
-                                            + $682
-                                        </Typography>
-                                        <Typography variant="h6" color="secondary" noWrap>
-                                            16%
-                                        </Typography>
-                                    </Stack>
-                                </ListItemSecondaryAction>
-                            </ListItemButton>
-                        </List>
-                    </MainCard>
-                    <MainCard sx={{ mt: 2 }}>
-                        <Stack spacing={3}>
-                            <Grid container justifyContent="space-between" alignItems="center">
-                                <Grid item>
-                                    <Stack>
-                                        <Typography variant="h5" noWrap>
-                                            Help & Support Chat
-                                        </Typography>
-                                        <Typography variant="caption" color="secondary" noWrap>
-                                            Typical replay within 5 min
-                                        </Typography>
-                                    </Stack>
-                                </Grid>
-                                <Grid item>
-                                    <AvatarGroup sx={{ '& .MuiAvatar-root': { width: 32, height: 32 } }}>
-                                        <Avatar alt="Remy Sharp" src={avatar1} />
-                                        <Avatar alt="Travis Howard" src={avatar2} />
-                                        <Avatar alt="Cindy Baker" src={avatar3} />
-                                        <Avatar alt="Agnes Walker" src={avatar4} />
-                                    </AvatarGroup>
-                                </Grid>
-                            </Grid>
-                            <Button size="small" variant="contained" sx={{ textTransform: 'capitalize' }}>
-                                Need Help?
-                            </Button>
-                        </Stack>
-                    </MainCard>
-                </Grid>
-                 */}
             </Grid>
         </div>}
       </>

+ 5 - 0
client/src/pages/servapps/containers/index.jsx

@@ -18,6 +18,7 @@ import NetworkContainerSetup from './network';
 import VolumeContainerSetup from './volumes';
 import DockerTerminal from './terminal';
 import ContainerMetrics from '../../dashboard/containerMetrics';
+import EventExplorerStandalone from '../../dashboard/eventsExplorerStandalone';
 
 const ContainerIndex = () => {
   const { containerName } = useParams();
@@ -64,6 +65,10 @@ const ContainerIndex = () => {
           title: 'Monitoring',
           children: <ContainerMetrics containerName={containerName}/>
         },
+        {
+          title: 'Events',
+          children: <EventExplorerStandalone initSearch={`{"object":"container@${containerName}"}`}/>
+        },
         {
           title: 'Terminal',
           children: <DockerTerminal refresh={refreshContainer} containerInfo={container} config={config}/>

+ 2 - 2
client/src/themes/palette.jsx

@@ -62,7 +62,7 @@ const Palette = (mode, PrimaryColor, SecondaryColor) => {
                 paper: paletteColor.grey[700],
                 default: paletteColor.grey[800]
             }
-        }
+        },
     } : {
         palette: {
             mode,
@@ -84,7 +84,7 @@ const Palette = (mode, PrimaryColor, SecondaryColor) => {
                 paper: paletteColor.grey[0],
                 default: paletteColor.grey.A50
             }
-        }
+        },
     });
 };
 

+ 16 - 0
client/src/themes/theme/index.jsx

@@ -64,6 +64,22 @@ const Theme = (colors, darkMode) => {
             darker: green[9],
             contrastText
         },
+        debug: {
+          lighter: grey[0],
+          light: grey[3],
+          main: grey[5],
+          dark: grey[7],
+          darker: grey[9],
+          contrastText
+        },
+        important: {
+          lighter: pink['100'],
+          light: pink['200'],
+          main: pink['400'],
+          dark: pink['700'],
+          darker: pink['800'],
+          contrastText
+        },
         grey: greyColors
     };
 };

+ 195 - 31
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cosmos-server",
-  "version": "0.12.0-unstable40",
+  "version": "0.12.0-unstable42",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cosmos-server",
-      "version": "0.12.0-unstable40",
+      "version": "0.12.0-unstable42",
       "dependencies": {
         "@ant-design/colors": "^6.0.0",
         "@ant-design/icons": "^4.7.0",
@@ -16,6 +16,7 @@
         "@jamesives/github-sponsors-readme-action": "^1.2.2",
         "@mui/lab": "^5.0.0-alpha.100",
         "@mui/material": "^5.10.6",
+        "@mui/x-date-pickers": "^6.18.0",
         "@reduxjs/toolkit": "^1.8.5",
         "@testing-library/jest-dom": "^5.16.5",
         "@testing-library/react": "^13.4.0",
@@ -24,6 +25,8 @@
         "apexcharts": "^3.35.5",
         "bcryptjs": "^2.4.3",
         "browserslist": "^4.21.7",
+        "date-fns": "^2.30.0",
+        "dayjs": "^1.11.10",
         "dot": "^1.1.3",
         "express": "^4.18.2",
         "formik": "^2.2.9",
@@ -2104,11 +2107,11 @@
       "dev": true
     },
     "node_modules/@babel/runtime": {
-      "version": "7.22.3",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz",
-      "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==",
+      "version": "7.23.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
+      "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
       "dependencies": {
-        "regenerator-runtime": "^0.13.11"
+        "regenerator-runtime": "^0.14.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -2127,6 +2130,11 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/runtime/node_modules/regenerator-runtime": {
+      "version": "0.14.0",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
+      "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
+    },
     "node_modules/@babel/template": {
       "version": "7.21.9",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.21.9.tgz",
@@ -2732,6 +2740,40 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
+      "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
+      "dependencies": {
+        "@floating-ui/utils": "^0.1.3"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz",
+      "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==",
+      "dependencies": {
+        "@floating-ui/core": "^1.4.2",
+        "@floating-ui/utils": "^0.1.3"
+      }
+    },
+    "node_modules/@floating-ui/react-dom": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz",
+      "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==",
+      "dependencies": {
+        "@floating-ui/dom": "^1.5.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0",
+        "react-dom": ">=16.8.0"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
+      "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
+    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.10",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@@ -3270,11 +3312,11 @@
       }
     },
     "node_modules/@mui/types": {
-      "version": "7.2.4",
-      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
-      "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
+      "version": "7.2.8",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.8.tgz",
+      "integrity": "sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==",
       "peerDependencies": {
-        "@types/react": "*"
+        "@types/react": "^17.0.0 || ^18.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -3283,13 +3325,12 @@
       }
     },
     "node_modules/@mui/utils": {
-      "version": "5.13.1",
-      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.1.tgz",
-      "integrity": "sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==",
+      "version": "5.14.17",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.17.tgz",
+      "integrity": "sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==",
       "dependencies": {
-        "@babel/runtime": "^7.21.0",
-        "@types/prop-types": "^15.7.5",
-        "@types/react-is": "^18.2.0",
+        "@babel/runtime": "^7.23.2",
+        "@types/prop-types": "^15.7.9",
         "prop-types": "^15.8.1",
         "react-is": "^18.2.0"
       },
@@ -3301,7 +3342,117 @@
         "url": "https://opencollective.com/mui"
       },
       "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0",
         "react": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/x-date-pickers": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.0.tgz",
+      "integrity": "sha512-y4UlkHQXiNRfb6FWQ/GWir0sZ+9kL+GEEZssG+XWP3KJ+d3lONRteusl4AJkYJBdIAOh+5LnMV9RAQKq9Sl7yw==",
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "@mui/base": "^5.0.0-beta.22",
+        "@mui/utils": "^5.14.16",
+        "@types/react-transition-group": "^4.4.8",
+        "clsx": "^2.0.0",
+        "prop-types": "^15.8.1",
+        "react-transition-group": "^4.4.5"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.9.0",
+        "@emotion/styled": "^11.8.1",
+        "@mui/material": "^5.8.6",
+        "@mui/system": "^5.8.0",
+        "date-fns": "^2.25.0",
+        "date-fns-jalali": "^2.13.0-0",
+        "dayjs": "^1.10.7",
+        "luxon": "^3.0.2",
+        "moment": "^2.29.4",
+        "moment-hijri": "^2.1.2",
+        "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+        "react": "^17.0.0 || ^18.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        },
+        "date-fns": {
+          "optional": true
+        },
+        "date-fns-jalali": {
+          "optional": true
+        },
+        "dayjs": {
+          "optional": true
+        },
+        "luxon": {
+          "optional": true
+        },
+        "moment": {
+          "optional": true
+        },
+        "moment-hijri": {
+          "optional": true
+        },
+        "moment-jalaali": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/x-date-pickers/node_modules/@mui/base": {
+      "version": "5.0.0-beta.23",
+      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.23.tgz",
+      "integrity": "sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw==",
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "@floating-ui/react-dom": "^2.0.2",
+        "@mui/types": "^7.2.8",
+        "@mui/utils": "^5.14.17",
+        "@popperjs/core": "^2.11.8",
+        "clsx": "^2.0.0",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0",
+        "react": "^17.0.0 || ^18.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/x-date-pickers/node_modules/clsx": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+      "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+      "engines": {
+        "node": ">=6"
       }
     },
     "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
@@ -3816,9 +3967,9 @@
       "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
     },
     "node_modules/@types/prop-types": {
-      "version": "15.7.5",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
-      "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+      "version": "15.7.9",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz",
+      "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
     },
     "node_modules/@types/react": {
       "version": "18.2.8",
@@ -3838,18 +3989,10 @@
         "@types/react": "*"
       }
     },
-    "node_modules/@types/react-is": {
-      "version": "18.2.0",
-      "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.0.tgz",
-      "integrity": "sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==",
-      "dependencies": {
-        "@types/react": "*"
-      }
-    },
     "node_modules/@types/react-transition-group": {
-      "version": "4.4.6",
-      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
-      "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
+      "version": "4.4.8",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz",
+      "integrity": "sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==",
       "dependencies": {
         "@types/react": "*"
       }
@@ -5011,6 +5154,26 @@
       "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
       "dev": true
     },
+    "node_modules/date-fns": {
+      "version": "2.30.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+      "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0"
+      },
+      "engines": {
+        "node": ">=0.11"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/date-fns"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.10",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+      "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
+    },
     "node_modules/debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -9308,7 +9471,8 @@
     "node_modules/regenerator-runtime": {
       "version": "0.13.11",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
-      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+      "dev": true
     },
     "node_modules/regenerator-transform": {
       "version": "0.15.1",

+ 4 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.12.0-unstable42",
+  "version": "0.12.0-unstable43",
   "description": "",
   "main": "test-server.js",
   "bugs": {
@@ -16,6 +16,7 @@
     "@jamesives/github-sponsors-readme-action": "^1.2.2",
     "@mui/lab": "^5.0.0-alpha.100",
     "@mui/material": "^5.10.6",
+    "@mui/x-date-pickers": "^6.18.0",
     "@reduxjs/toolkit": "^1.8.5",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
@@ -24,6 +25,8 @@
     "apexcharts": "^3.35.5",
     "bcryptjs": "^2.4.3",
     "browserslist": "^4.21.7",
+    "date-fns": "^2.30.0",
+    "dayjs": "^1.11.10",
     "dot": "^1.1.3",
     "express": "^4.18.2",
     "formik": "^2.2.9",

+ 1 - 1
src/CRON.go

@@ -109,7 +109,7 @@ func checkCerts() {
 		HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["SELFSIGNED"] ||
 		HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
 		utils.Log("Checking certificates for renewal")
-		if !CertificateIsValid(HTTPConfig.TLSValidUntil) {
+		if !CertificateIsExpiredSoon(HTTPConfig.TLSValidUntil) {
 			utils.Log("Certificates are not valid anymore, renewing")
 			RestartServer()
 		}

+ 20 - 1
src/docker/docker.go

@@ -537,6 +537,15 @@ func CheckUpdatesAvailable() map[string]bool {
 		}
 
 		if needsUpdate && HasAutoUpdateOn(fullContainer) {
+			utils.TriggerEvent(
+				"cosmos.docker.container.update",
+				"Cosmos Container Update",
+				"success",
+				"",
+				map[string]interface{}{
+					"container": container.Names[0][1:],
+			})
+
 			utils.WriteNotification(utils.Notification{
 				Recipient: "admin",
 				Title: "Container Update",
@@ -612,6 +621,16 @@ func SelfRecreate() error {
 		Services: map[string]ContainerCreateRequestContainer {},
 	}
 
+	utils.TriggerEvent(
+		"cosmos.internal.self-updater",
+		"Cosmos Self Updater",
+		"important",
+		"",
+		map[string]interface{}{
+			"action": "recreate",
+			"container": containerName,
+	})
+
 	service.Services["cosmos-self-updater-agent"] = ContainerCreateRequestContainer{
 		Name: "cosmos-self-updater-agent",
 		Image: "azukaar/docker-self-updater:" + version,
@@ -756,7 +775,7 @@ func Stats(container types.Container) (ContainerStats, error) {
 			utils.Error("StatsAll", err)
 			return nil, err
 		}
-	
+
 		var containerStatsList []ContainerStats
 		var wg sync.WaitGroup
 		semaphore := make(chan struct{}, 5) // A channel with a buffer size of 5 for controlling parallelism.

+ 37 - 1
src/docker/events.go

@@ -35,7 +35,7 @@ func DockerListenEvents() error {
 					msgs, errs = DockerClient.Events(context.Background(), types.EventsOptions{})
 
 				case msg := <-msgs:
-					utils.Debug("Docker Event: " + msg.Type + " " + msg.Action + " " + msg.Actor.ID)
+					utils.Debug("Docker Event: " + msg.Type + " " + msg.Action + " " + msg.Actor.Attributes["name"])
 					if msg.Type == "container" && msg.Action == "start" {
 						onDockerStarted(msg.Actor.ID)
 					}
@@ -59,6 +59,42 @@ func DockerListenEvents() error {
 					if msg.Type == "network" && msg.Action == "connect" {
 						onNetworkConnect(msg.Actor.ID)
 					}
+
+					level := "info"
+					if msg.Type == "image" {
+						level = "debug"
+					}
+					if msg.Action == "destroy" || msg.Action == "delete" || msg.Action == "kill" || msg.Action == "die" {
+						level = "warning"
+					}
+					if msg.Action == "create" || msg.Action == "start" {
+						level = "success"
+					}
+					
+					object := ""
+					if msg.Type == "container" {
+						object = "container@" + msg.Actor.Attributes["name"]
+					} else if msg.Type == "network" {
+						object = "network@" + msg.Actor.Attributes["name"]
+					} else if msg.Type == "image" {
+						object = "image@" + msg.Actor.Attributes["name"]
+					} else if msg.Type == "volume" && msg.Actor.Attributes["name"] != "" {
+						object = "volume@" + msg.Actor.Attributes["name"]
+					}
+					
+					utils.TriggerEvent(
+						"cosmos.docker.event." + msg.Type + "." + msg.Action,
+						"Docker Event " + msg.Type + " " + msg.Action,
+						level,
+						object,
+						map[string]interface{}{
+						"Type": msg.Type,
+						"Action": msg.Action,
+						"Actor": msg.Actor,
+						"Status": msg.Status,
+						"From": msg.From,
+						"Scope": msg.Scope,
+					})
 			}
 		}
 	}()

+ 55 - 4
src/httpServer.go

@@ -175,9 +175,26 @@ func SecureAPI(userRouter *mux.Router, public bool) {
 	))
 }
 
-func CertificateIsValid(validUntil time.Time) bool {
+func CertificateIsExpiredSoon(validUntil time.Time) bool {
 	// allow 5 days of leeway
-	isValid := time.Now().Add(5 * 24 * time.Hour).Before(validUntil)
+	isValid := time.Now().Add(45 * 24 * time.Hour).Before(validUntil)
+	if !isValid {
+		utils.TriggerEvent(
+			"cosmos.proxy.certificate",
+			"Cosmos Certificate Expire Soon",
+			"warning",
+			"",
+			map[string]interface{}{
+		})
+
+		utils.Log("Certificate is not valid anymore. Needs refresh")
+	}
+	return isValid
+}
+
+func CertificateIsExpired(validUntil time.Time) bool {
+	// allow 5 days of leeway
+	isValid := time.Now().Before(validUntil)
 	if !isValid {
 		utils.Log("Certificate is not valid anymore. Needs refresh")
 	}
@@ -200,13 +217,13 @@ func InitServer() *mux.Router {
 	oldDomains := baseMainConfig.HTTPConfig.TLSKeyHostsCached
 	falledBack := false
 
-	NeedsRefresh := baseMainConfig.HTTPConfig.ForceHTTPSCertificateRenewal || (tlsCert == "" || tlsKey == "") || utils.HasAnyNewItem(domains, oldDomains) || !CertificateIsValid(baseMainConfig.HTTPConfig.TLSValidUntil)
+	NeedsRefresh := baseMainConfig.HTTPConfig.ForceHTTPSCertificateRenewal || (tlsCert == "" || tlsKey == "") || utils.HasAnyNewItem(domains, oldDomains) || !CertificateIsExpiredSoon(baseMainConfig.HTTPConfig.TLSValidUntil)
 	
 	// If we have a certificate, we can fallback to it if necessary
 	CanFallback := tlsCert != "" && tlsKey != "" && 
 		len(config.HTTPConfig.TLSKeyHostsCached) > 0 && 
 		config.HTTPConfig.TLSKeyHostsCached[0] == config.HTTPConfig.Hostname  &&
-		CertificateIsValid(baseMainConfig.HTTPConfig.TLSValidUntil)
+		CertificateIsExpired(baseMainConfig.HTTPConfig.TLSValidUntil)
 
 	if(NeedsRefresh && config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
 		if(config.HTTPConfig.DNSChallengeProvider != "") {
@@ -237,6 +254,22 @@ func InitServer() *mux.Router {
 			utils.SetBaseMainConfig(baseMainConfig)
 			utils.Log("Saved new LETSENCRYPT TLS certificate")
 	
+			utils.TriggerEvent(
+				"cosmos.proxy.certificate",
+				"Cosmos Certificate Renewed",
+				"important",
+				"",
+				map[string]interface{}{
+					"domains": domains,
+			})
+
+			utils.WriteNotification(utils.Notification{
+				Recipient: "admin",
+				Title: "Cosmos Certificate Renewed",
+				Message: "The TLS certificate for the following domains has been renewed: " + strings.Join(domains, ", "),
+				Level: "info",
+			})
+
 			tlsCert = pub
 			tlsKey = priv
 		}
@@ -255,6 +288,22 @@ func InitServer() *mux.Router {
 
 			utils.SetBaseMainConfig(baseMainConfig)
 			utils.Log("Saved new SELFISGNED TLS certificate")
+
+			utils.TriggerEvent(
+				"cosmos.proxy.certificate",
+				"Cosmos Certificate Renewed",
+				"important",
+				"",
+				map[string]interface{}{
+					"domains": domains,
+			})
+
+			utils.WriteNotification(utils.Notification{
+				Recipient: "admin",
+				Title: "Cosmos Certificate Renewed",
+				Message: "The TLS certificate for the following domains has been renewed: " + strings.Join(domains, ", "),
+				Level: "info",
+			})
 		}
 
 		tlsCert = pub
@@ -351,6 +400,8 @@ func InitServer() *mux.Router {
 	srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
 	srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
 
+	srapi.HandleFunc("/api/events", metrics.API_ListEvents)
+
 	srapi.HandleFunc("/api/metrics", metrics.API_GetMetrics)
 	srapi.HandleFunc("/api/reset-metrics", metrics.API_ResetMetrics)
 	srapi.HandleFunc("/api/list-metrics", metrics.ListMetrics)

+ 1 - 1
src/metrics/aggl.go

@@ -16,7 +16,6 @@ type DataDefDBEntry struct {
 	Date time.Time
 	Value int
 	Processed bool
-
 	// For agglomeration
 	AvgIndex int
 	AggloTo time.Time
@@ -27,6 +26,7 @@ type DataDefDB struct {
 	Values []DataDefDBEntry
 	ValuesAggl map[string]DataDefDBEntry
 	LastUpdate time.Time
+	TimeScale float64
 	Max uint64
 	Label string
 	Key string

+ 72 - 0
src/metrics/api.go

@@ -4,6 +4,9 @@ import (
 	"net/http"
 	"encoding/json"
 	"strings"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo/options"
 	
 	"github.com/azukaar/cosmos-server/src/utils"
 )
@@ -58,6 +61,21 @@ func API_ResetMetrics(w http.ResponseWriter, req *http.Request) {
 		return
 	}
 
+	c, errCo = utils.GetCollection(utils.GetRootAppId(), "events")
+	if errCo != nil {
+		utils.Error("MetricsReset: Database error" , errCo)
+		utils.HTTPError(w, "Database error ", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+
+	// delete all metrics from database
+	_, err = c.DeleteMany(nil, map[string]interface{}{})
+	if err != nil {
+		utils.Error("MetricsReset: Database error ", err)
+		utils.HTTPError(w, "Database error ", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+
 	if(req.Method == "GET") {
 		json.NewEncoder(w).Encode(map[string]interface{}{
 			"status": "OK",
@@ -67,4 +85,58 @@ func API_ResetMetrics(w http.ResponseWriter, req *http.Request) {
 		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
 		return
 	}
+}
+
+type MetricList struct {
+	Key string
+	Label string
+}
+
+func ListMetrics(w http.ResponseWriter, req *http.Request) {
+	if utils.AdminOnly(w, req) != nil {
+		return
+	}
+	
+	if(req.Method == "GET") {
+		c, errCo := utils.GetCollection(utils.GetRootAppId(), "metrics")
+		if errCo != nil {
+				utils.Error("Database Connect", errCo)
+				utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
+				return
+		}
+
+		metrics := []MetricList{}
+
+		cursor, err := c.Find(nil, map[string]interface{}{}, options.Find().SetProjection(bson.M{"Key": 1, "Label":1, "_id": 0}))
+
+		if err != nil {
+			utils.Error("metrics: Error while getting metrics", err)
+			utils.HTTPError(w, "metrics Get Error", http.StatusInternalServerError, "UD001")
+			return
+		}
+
+		defer cursor.Close(nil)
+
+		if err = cursor.All(nil, &metrics); err != nil {
+			utils.Error("metrics: Error while decoding metrics", err)
+			utils.HTTPError(w, "metrics decode Error", http.StatusInternalServerError, "UD002")
+			return
+		}
+
+		// Extract the names into a string slice
+		metricNames := map[string]string{}
+
+		for _, metric := range metrics {
+				metricNames[metric.Key] = metric.Label
+		}
+
+		json.NewEncoder(w).Encode(map[string]interface{}{
+				"status": "OK",
+				"data":   metricNames,
+		})
+	} else {
+		utils.Error("metrics: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
 }

+ 152 - 0
src/metrics/events.go

@@ -0,0 +1,152 @@
+package metrics 
+
+import (
+	"net/http"
+	"encoding/json"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.mongodb.org/mongo-driver/mongo/options"
+
+	"github.com/azukaar/cosmos-server/src/utils"
+)
+
+type Event struct {
+	Id primitive.ObjectID `json:"id" bson:"_id"`
+	Label string `json:"label" bson:"label"`
+	Application string `json:"application" bson:"application"`
+	EventId string `json:"eventId" bson:"eventId"`
+	Date time.Time `json:"date" bson:"date"`
+	Level string `json:"level" bson:"level"`
+	Data map[string]interface{} `json:"data" bson:"data"`
+	Object string `json:"object" bson:"object"`
+}
+
+func API_ListEvents(w http.ResponseWriter, req *http.Request) {
+	if utils.AdminOnly(w, req) != nil {
+		return
+	}
+	
+	if(req.Method == "GET") {
+
+		query := req.URL.Query()
+		from, errF := time.Parse("2006-01-02T15:04:05Z", query.Get("from"))
+		if errF != nil {
+			utils.Error("events: Error while parsing from date", errF)
+		}
+		to, errF := time.Parse("2006-01-02T15:04:05Z", query.Get("to"))
+		if errF != nil {
+			utils.Error("events: Error while parsing from date", errF)
+		}
+
+		logLevel := query.Get("logLevel")
+		if logLevel == "" {
+			logLevel = "info"
+		}
+		search := query.Get("search")
+		dbQuery := query.Get("query")
+
+		page := query.Get("page")
+		var pageId primitive.ObjectID
+		if page != "" {
+			pageId, _ = primitive.ObjectIDFromHex(page)
+		}
+
+		// decode to bson
+		dbQueryBson := bson.M{}
+		if dbQuery != "" {
+			err := bson.UnmarshalExtJSON([]byte(dbQuery), true, &dbQueryBson)
+			if err != nil {
+				utils.Error("events: Error while parsing query " + dbQuery, err)
+				utils.HTTPError(w, "events Get Error", http.StatusInternalServerError, "UD001")
+				return
+			}
+		} else if search != "" {
+			dbQueryBson["$text"] = bson.M{
+				"$search": search,
+			}
+		}
+
+	
+		// merge date query into dbQueryBson
+		if dbQueryBson["date"] == nil {
+			dbQueryBson["date"] = bson.M{}
+		}
+		dbQueryBson["date"].(bson.M)["$gte"] = from
+		dbQueryBson["date"].(bson.M)["$lte"] = to
+
+		if logLevel != "" {
+			if dbQueryBson["level"] == nil {
+				dbQueryBson["level"] = bson.M{}
+			}
+			levels := []string{"error"}
+			if logLevel == "debug" {
+				levels = []string{"debug", "info", "warning", "error", "important", "success"}
+			} else if logLevel == "info" {
+				levels = []string{"info", "warning", "error", "important", "success"}
+			} else if logLevel == "success" {
+				levels = []string{"warning", "error", "important", "success"}
+			} else if logLevel == "warning" {
+				levels = []string{"warning", "error", "important"}
+			} else if logLevel == "important" {
+				levels = []string{"important", "error"}
+			}
+			dbQueryBson["level"].(bson.M)["$in"] = levels
+		}
+
+		if pageId != primitive.NilObjectID {
+			dbQueryBson["_id"] = bson.M{
+				"$lt": pageId,
+			}
+		}
+		
+		c, errCo := utils.GetCollection(utils.GetRootAppId(), "events")
+		if errCo != nil {
+				utils.Error("Database Connect", errCo)
+				utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
+				return
+		}
+
+		events := []Event{}
+
+		limit := int64(50)
+		opts := options.Find().SetLimit(limit).SetSort(bson.D{{"date", -1}})
+		// .SetProjection(bson.D{{"_id", 1}, {"eventId", 1}, {"date", 1}, {"level", 1}, {"data", 1}})
+
+		cursor, err := c.Find(nil, dbQueryBson, opts)
+
+		if err != nil {
+			utils.Error("events: Error while getting events", err)
+			utils.HTTPError(w, "events Get Error", http.StatusInternalServerError, "UD001")
+			return
+		}
+
+		defer cursor.Close(nil)
+
+		if err = cursor.All(nil, &events); err != nil {
+			utils.Error("events: Error while decoding events", err)
+			utils.HTTPError(w, "events decode Error", http.StatusInternalServerError, "UD002")
+			return
+		}
+
+		totalCount, err := c.CountDocuments(nil, dbQueryBson)
+		if err != nil {
+			utils.Error("events: Error while counting events", err)
+			utils.HTTPError(w, "events count Error", http.StatusInternalServerError, "UD003")
+			return
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+
+		json.NewEncoder(w).Encode(map[string]interface{}{
+				"status": "OK",
+				"total": totalCount,
+				"data":   events,
+		})
+	} else {
+		utils.Error("events: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}

+ 0 - 59
src/metrics/http.go

@@ -2,11 +2,6 @@ package metrics
 
 import (
 	"time"
-	"net/http"
-	"encoding/json"
-
-	"go.mongodb.org/mongo-driver/mongo/options"
-	"go.mongodb.org/mongo-driver/bson"
 
 	"github.com/azukaar/cosmos-server/src/utils"
 )
@@ -118,57 +113,3 @@ func PushShieldMetrics(reason string) {
 		SetOperation: "sum",
 	})
 }
-
-type MetricList struct {
-	Key string
-	Label string
-}
-
-func ListMetrics(w http.ResponseWriter, req *http.Request) {
-	if utils.AdminOnly(w, req) != nil {
-		return
-	}
-	
-	if(req.Method == "GET") {
-		c, errCo := utils.GetCollection(utils.GetRootAppId(), "metrics")
-		if errCo != nil {
-				utils.Error("Database Connect", errCo)
-				utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
-				return
-		}
-
-		metrics := []MetricList{}
-
-		cursor, err := c.Find(nil, map[string]interface{}{}, options.Find().SetProjection(bson.M{"Key": 1, "Label":1, "_id": 0}))
-
-		if err != nil {
-			utils.Error("metrics: Error while getting metrics", err)
-			utils.HTTPError(w, "metrics Get Error", http.StatusInternalServerError, "UD001")
-			return
-		}
-
-		defer cursor.Close(nil)
-
-		if err = cursor.All(nil, &metrics); err != nil {
-			utils.Error("metrics: Error while decoding metrics", err)
-			utils.HTTPError(w, "metrics decode Error", http.StatusInternalServerError, "UD002")
-			return
-		}
-
-		// Extract the names into a string slice
-		metricNames := map[string]string{}
-
-		for _, metric := range metrics {
-				metricNames[metric.Key] = metric.Label
-		}
-
-		json.NewEncoder(w).Encode(map[string]interface{}{
-				"status": "OK",
-				"data":   metricNames,
-		})
-	} else {
-		utils.Error("metrics: Method not allowed" + req.Method, nil)
-		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
-		return
-	}
-}

+ 5 - 3
src/metrics/index.go

@@ -27,6 +27,7 @@ type DataPush struct {
 	Key string
 	Value int
 	Max uint64
+	Period time.Duration
 	Expire time.Time
 	Label string
 	AvgIndex int
@@ -112,6 +113,7 @@ func SaveMetrics() {
 							"Scale": scale,
 							"Unit": dp.Unit,
 							"Object": dp.Object,
+							"TimeScale": float64(dp.Period / (time.Second * 30)),
 					},
 			}
 			
@@ -191,6 +193,7 @@ func PushSetMetric(key string, value int, def DataDef) {
 				Scale: def.Scale,
 				Unit: def.Unit,
 				Object: def.Object,
+				Period: def.Period,
 			}
 		}
 
@@ -199,11 +202,10 @@ func PushSetMetric(key string, value int, def DataDef) {
 }
 
 func Run() {
-	utils.Debug("Metrics - Run")
-
 	nextTime := ModuloTime(time.Now().Add(time.Second*30), time.Second*30)
 	nextTime = nextTime.Add(time.Second * 2)
-	utils.Debug("Metrics - Next run at " + nextTime.String())
+
+	utils.Debug("Metrics - Run - Next run at " + nextTime.String())
 	
 	if utils.GetMainConfig().MonitoringDisabled {
 		time.AfterFunc(nextTime.Sub(time.Now()), func() {

+ 43 - 0
src/proxy/shield.go

@@ -319,6 +319,27 @@ func SmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) func(h
 				wrapper.TimeEnded = time.Now()
 				wrapper.isOver = true
 
+				statusText := "success"
+				level := "info"
+				if wrapper.Status >= 400 {
+					statusText = "error"
+					level = "warning"
+				}
+
+				utils.TriggerEvent(
+					"cosmos.proxy.response." + route.Name + "." + statusText,
+					"Proxy Response " + route.Name + " " + statusText,
+					level,
+					"route@" + route.Name,
+					map[string]interface{}{
+					"Route": route.Name,
+					"Status": wrapper.Status,
+					"Method": wrapper.Method,
+					"ClientID": wrapper.ClientID,
+					"Time": wrapper.TimeEnded.Sub(wrapper.TimeStarted).Seconds(),
+					"Bytes": wrapper.Bytes,
+				})
+
 				go metrics.PushRequestMetrics(route, wrapper.Status, wrapper.TimeStarted, wrapper.Bytes)
 
 				return
@@ -400,6 +421,28 @@ func SmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) func(h
 					shield.Lock()
 					wrapper.TimeEnded = time.Now()
 					wrapper.isOver = true
+
+					statusText := "success"
+					level := "info"
+					if wrapper.Status >= 400 {
+						statusText = "error"
+						level = "warning"
+					}
+					
+					utils.TriggerEvent(
+						"cosmos.proxy.response." + route.Name + "." + statusText,
+						"Proxy Response " + route.Name + " " + statusText,
+						level,
+						"route@" + route.Name,
+						map[string]interface{}{
+						"Route": route.Name,
+						"Status": wrapper.Status,
+						"Method": wrapper.Method,
+						"ClientID": wrapper.ClientID,
+						"Time": wrapper.TimeEnded.Sub(wrapper.TimeStarted).Seconds(),
+						"Bytes": wrapper.Bytes,
+					})
+
 					go metrics.PushRequestMetrics(route, wrapper.Status, wrapper.TimeStarted, wrapper.Bytes)
 					shield.Unlock()
 				})()

+ 24 - 2
src/utils/db.go

@@ -10,7 +10,8 @@ import (
 	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/options"
 	"go.mongodb.org/mongo-driver/mongo/readpref"
-	"go.mongodb.org/mongo-driver/mongo/writeconcern" 
+	"go.mongodb.org/mongo-driver/mongo/writeconcern"
+	"go.mongodb.org/mongo-driver/bson" 
 )
 
 
@@ -50,6 +51,8 @@ func DB() error {
 		return err
 	}
 
+	initDB()
+
 	Log("Successfully connected to the database.")
 	return nil
 }
@@ -73,7 +76,7 @@ func GetCollection(applicationId string, collection string) (*mongo.Collection,
 		name = "COSMOS"
 	}
 	
-	Debug("Getting collection " + applicationId + "_" + collection + " from database " + name)
+	// Debug("Getting collection " + applicationId + "_" + collection + " from database " + name)
 	
 	c := client.Database(name).Collection(applicationId + "_" + collection)
 	
@@ -202,4 +205,23 @@ func ListAllUsers(role string) []User {
 	}
 
 	return users
+}
+
+func initDB() {
+	c, errCo := GetCollection(GetRootAppId(), "events")
+	if errCo != nil {
+		Error("Metrics - Database Connect", errCo)
+	} else {
+		// Create a text index on the _search field
+		model := mongo.IndexModel{
+			Keys: bson.M{"_search": "text"}, // Specify the field to index here
+		}
+
+		// Creating the index
+		_, err := c.Indexes().CreateOne(context.Background(), model)
+		if err != nil {
+			Error("Metrics - Create Index", err)
+			return // Handle error appropriately
+		}
+	}
 }

+ 31 - 0
src/utils/events.go

@@ -0,0 +1,31 @@
+package utils 
+
+import (
+	"time"
+	"encoding/json"
+)
+
+func TriggerEvent(eventId string, label string, level string, object string, data map[string]interface{}) {
+	Debug("Triggering event " + eventId)
+
+
+	// Marshal the data map into a JSON string
+	dataAsBytes, err := json.Marshal(data)
+	if err != nil {
+		Error("Error marshaling data: %v\n", err)
+		return
+	}
+	dataAsString := string(dataAsBytes)
+
+	BufferedDBWrite("events", map[string]interface{}{
+		"eventId": eventId,
+		"label": label,
+		"application": "Cosmos",
+		"level": level,
+		"date": time.Now(),
+		"data": data,
+		"object": object,
+		"_search": eventId + " " + dataAsString,
+	})
+}
+

+ 1 - 8
src/utils/middleware.go

@@ -203,16 +203,11 @@ func BlockByCountryMiddleware(blockedCountries []string, CountryBlacklistIsWhite
 			countryCode, err := GetIPLocation(ip)
 
 			if err == nil {
-				if countryCode == "" {
-					Debug("Country code is empty")
-				} else {
-					Debug("Country code: " + countryCode)
-				}
-
 				config := GetMainConfig()
 
 				if CountryBlacklistIsWhitelist {
 					if countryCode != "" {
+						Debug("Country code: " + countryCode)
 						blocked := true
 						for _, blockedCountry := range blockedCountries {
 							if config.ServerCountry != countryCode && countryCode == blockedCountry {
@@ -272,8 +267,6 @@ func BlockPostWithoutReferer(next http.Handler) http.Handler {
 
 func EnsureHostname(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		Debug("Ensuring origin for requested resource from : " + r.Host)
-
 		og := GetMainConfig().HTTPConfig.Hostname
 		ni := GetMainConfig().NewInstall
 

+ 0 - 1
src/utils/utils.go

@@ -383,7 +383,6 @@ func GetAllHostnames(applyWildCard bool, removePorts bool) []string {
 		uniqueHostnames = filteredHostnames
 	}
 
-	Debug("Hostnames are " + strings.Join(uniqueHostnames, ", "))
 	return uniqueHostnames
 }