[release] v0.12.0-unstable43
This commit is contained in:
parent
34bc76dbf8
commit
050fe7484b
|
@ -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,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 />}
|
||||
>
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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'}}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
|
@ -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>}
|
||||
</>
|
||||
|
|
|
@ -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}/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
})()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
@ -383,7 +383,6 @@ func GetAllHostnames(applyWildCard bool, removePorts bool) []string {
|
|||
uniqueHostnames = filteredHostnames
|
||||
}
|
||||
|
||||
Debug("Hostnames are " + strings.Join(uniqueHostnames, ", "))
|
||||
return uniqueHostnames
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue