[release] v0.12.0-unstable43

This commit is contained in:
Yann Stepienik 2023-11-06 19:57:04 +00:00
parent 34bc76dbf8
commit 050fe7484b
38 changed files with 1062 additions and 1195 deletions

View File

@ -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

View File

@ -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 />}
>

View File

@ -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
};

View File

@ -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>

View File

@ -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'}}>

View File

@ -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}

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;
}
})

View File

@ -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({

View File

@ -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,
});

View File

@ -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;

View File

@ -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>}
</>

View File

@ -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}/>

View File

@ -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
}
}
},
});
};

View File

@ -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
};
};

226
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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()
}

View File

@ -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.

View File

@ -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,
})
}
}
}()

View File

@ -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)

View File

@ -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

View File

@ -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
src/metrics/events.go Normal file
View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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() {

View File

@ -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()
})()

View File

@ -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
src/utils/events.go Normal file
View File

@ -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,
})
}

View File

@ -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

View File

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