mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-18 23:59:41 -06:00
Merge branch 'develop' into fix/fe/uptime-home
This commit is contained in:
@@ -1,130 +0,0 @@
|
||||
.MuiTable-root .host {
|
||||
width: fit-content;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.MuiTable-root .host span {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.MuiTable-root .label {
|
||||
line-height: 1;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
padding: 7px;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
|
||||
.MuiPaper-root:has(table.MuiTable-root) {
|
||||
box-shadow: none;
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.MuiTable-root .MuiTableHead-root .MuiTableCell-root,
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span {
|
||||
display: inline-block;
|
||||
height: 17px;
|
||||
width: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: -3px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1);
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.MuiPaper-root + .MuiPagination-root {
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul {
|
||||
justify-content: center;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button {
|
||||
padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button {
|
||||
padding: 0 var(--env-var-spacing-1-plus) 0 var(--env-var-spacing-1);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button::after,
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button::after {
|
||||
content: "Previous";
|
||||
margin-left: 15px;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
|
||||
content: "Next";
|
||||
margin-right: 15px;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.MuiTablePagination-root p {
|
||||
font-weight: 500;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select {
|
||||
text-align: left;
|
||||
text-align-last: left;
|
||||
}
|
||||
.MuiTablePagination-root button {
|
||||
min-width: 0;
|
||||
padding: 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.MuiTablePagination-root svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.MuiTablePagination-root .MuiSelect-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
right: 8%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.MuiTablePagination-root button.Mui-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.table-container .MuiTable-root .MuiTableHead-root .MuiTableCell-root {
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
font-weight: 400;
|
||||
}
|
||||
.monitors .MuiTableCell-root:not(:first-of-type):not(:last-of-type),
|
||||
.monitors .MuiTableCell-root:not(:first-of-type):not(:last-of-type) {
|
||||
padding-left: var(--env-var-spacing-1);
|
||||
padding-right: var(--env-var-spacing-1);
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TablePagination,
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../Features/UI/uiSlice";
|
||||
import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../assets/icons/right-arrow.svg?react";
|
||||
import SelectorVertical from "../../assets/icons/selector-vertical.svg?react";
|
||||
import "./index.css";
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.count - Total number of items.
|
||||
* @param {number} props.page - Current page number.
|
||||
* @param {number} props.rowsPerPage - Number of rows per page.
|
||||
* @param {function} props.onPageChange - Callback function to handle page change.
|
||||
*
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
const TablePaginationActions = (props) => {
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
const handleBackButtonClick = (event) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
const handleNextButtonClick = (event) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
const handleLastPageButtonClick = (event) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* BasicTable Component
|
||||
* Renders a table with optional pagination.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props.
|
||||
* @param {Object} props.data - Data for the table including columns and rows.
|
||||
* @param {Array} props.data.cols - Array of objects for column headers.
|
||||
* @param {number} props.data.cols[].id - Unique identifier for the column.
|
||||
* @param {string} props.data.cols[].name - Name of the column to display as header.
|
||||
* @param {Array} props.data.rows - Array of row objects.
|
||||
* @param {number} props.data.rows[].id - Unique identifier for the row.
|
||||
* @param {Array} props.data.rows[].data - Array of cell data objects for the row.
|
||||
* @param {number} props.data.rows[].data[].id - Unique identifier for the cell.
|
||||
* @param {JSX.Element} props.data.rows[].data[].data - The content to display in the cell.
|
||||
* @param {function} props.data.rows.data.handleClick - Function to call when the row is clicked.
|
||||
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
|
||||
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
|
||||
* @param {number} props.rowsPerPage- Number of rows per page (table).
|
||||
* @param {string} props.emptyMessage - Message to display when there is no data.
|
||||
* @example
|
||||
* const data = {
|
||||
* cols: [
|
||||
* { id: 1, name: "First Col" },
|
||||
* { id: 2, name: "Second Col" },
|
||||
* { id: 3, name: "Third Col" },
|
||||
* { id: 4, name: "Fourth Col" },
|
||||
* ],
|
||||
* rows: [
|
||||
* {
|
||||
* id: 1,
|
||||
* data: [
|
||||
* { id: 1, data: <div>Data for Row 1 Col 1</div> },
|
||||
* { id: 2, data: <div>Data for Row 1 Col 2</div> },
|
||||
* { id: 3, data: <div>Data for Row 1 Col 3</div> },
|
||||
* { id: 4, data: <div>Data for Row 1 Col 4</div> },
|
||||
* ],
|
||||
* },
|
||||
* {
|
||||
* id: 2,
|
||||
* data: [
|
||||
* { id: 5, data: <div>Data for Row 2 Col 1</div> },
|
||||
* { id: 6, data: <div>Data for Row 2 Col 2</div> },
|
||||
* { id: 7, data: <div>Data for Row 2 Col 3</div> },
|
||||
* { id: 8, data: <div>Data for Row 2 Col 4</div> },
|
||||
* ],
|
||||
* },
|
||||
* ],
|
||||
* };
|
||||
*
|
||||
* <BasicTable data={data} rows={rows} paginated={true} />
|
||||
*/
|
||||
|
||||
const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
|
||||
const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const uiState = useSelector((state) => state.ui);
|
||||
let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [data]);
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: table,
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
let displayData = [];
|
||||
|
||||
if (data && data.rows) {
|
||||
let rows = reversed ? [...data.rows].reverse() : data.rows;
|
||||
displayData = paginated
|
||||
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: rows;
|
||||
}
|
||||
|
||||
if (!data || !data.cols || !data.rows) {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the range of displayed rows.
|
||||
* @returns {string}
|
||||
*/
|
||||
const getRange = () => {
|
||||
let start = page * rowsPerPage + 1;
|
||||
let end = Math.min(page * rowsPerPage + rowsPerPage, data.rows.length);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{data.cols.map((col) => (
|
||||
<TableCell key={col.id}>{col.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: row.handleClick ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
filter: "brightness(.75)",
|
||||
opacity: 0.75,
|
||||
transition: "filter 0.3s ease, opacity 0.3s ease",
|
||||
},
|
||||
}}
|
||||
key={row.id}
|
||||
onClick={row.handleClick ? row.handleClick : null}
|
||||
>
|
||||
{row.data.map((cell) => {
|
||||
return <TableCell key={cell.id}>{cell.data}</TableCell>;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{displayData.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ textAlign: "center" }}
|
||||
colSpan={data.cols.length}
|
||||
>
|
||||
{emptyMessage}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginated && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
sx={{
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
px={theme.spacing(2)}
|
||||
fontSize={12}
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
Showing {getRange()} of {data.rows.length} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.rows.length}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 15, 25]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
labelRowsPerPage="Rows per page"
|
||||
labelDisplayedRows={({ page, count }) =>
|
||||
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
|
||||
}
|
||||
slotProps={{
|
||||
select: {
|
||||
MenuProps: {
|
||||
keepMounted: true,
|
||||
PaperProps: {
|
||||
className: "pagination-dropdown",
|
||||
sx: {
|
||||
mt: 0,
|
||||
mb: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: { mt: theme.spacing(-2) },
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
minWidth: theme.spacing(20),
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
mt: theme.spacing(6),
|
||||
color: theme.palette.text.secondary,
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BasicTable.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
paginated: PropTypes.bool,
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
table: PropTypes.string,
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BasicTable;
|
||||
@@ -13,9 +13,14 @@ import { useTheme } from "@emotion/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import "./index.css";
|
||||
import {
|
||||
tooltipDateFormatLookup,
|
||||
tickDateFormatLookup,
|
||||
} from "../Utils/chartUtilFunctions";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
import "./index.css";
|
||||
const CustomToolTip = ({ active, payload, label, dateRange }) => {
|
||||
const format = tooltipDateFormatLookup(dateRange);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
if (active && payload && payload.length) {
|
||||
@@ -41,7 +46,7 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
{formatDateWithTz(label, format, uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
@@ -102,13 +107,12 @@ CustomToolTip.propTypes = {
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const CustomTick = ({ x, y, payload, index, dateRange }) => {
|
||||
const format = tickDateFormatLookup(dateRange);
|
||||
const theme = useTheme();
|
||||
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
@@ -118,7 +122,7 @@ const CustomTick = ({ x, y, payload, index }) => {
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -128,9 +132,10 @@ CustomTick.propTypes = {
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
const MonitorDetailsAreaChart = ({ checks, dateRange }) => {
|
||||
const theme = useTheme();
|
||||
const memoizedChecks = useMemo(() => checks, [checks[0]]);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
@@ -184,16 +189,14 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="_id"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
tick={<CustomTick dateRange={dateRange} />}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip />}
|
||||
content={<CustomToolTip dateRange={dateRange} />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
@@ -211,6 +214,7 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
|
||||
MonitorDetailsAreaChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
export default MonitorDetailsAreaChart;
|
||||
|
||||
25
Client/src/Components/Charts/Utils/chartUtilFunctions.js
Normal file
25
Client/src/Components/Charts/Utils/chartUtilFunctions.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const tooltipDateFormatLookup = (dateRange) => {
|
||||
const dateFormatLookup = {
|
||||
day: "ddd. MMMM D, YYYY, hh:mm A",
|
||||
week: "ddd. MMMM D, YYYY, hh:mm A",
|
||||
month: "ddd. MMMM D, YYYY",
|
||||
};
|
||||
const format = dateFormatLookup[dateRange];
|
||||
if (format === undefined) {
|
||||
return "";
|
||||
}
|
||||
return format;
|
||||
};
|
||||
|
||||
export const tickDateFormatLookup = (dateRange) => {
|
||||
const tickFormatLookup = {
|
||||
day: "h:mm A",
|
||||
week: "MM/D, h:mm A",
|
||||
month: "ddd. M/D",
|
||||
};
|
||||
const format = tickFormatLookup[dateRange];
|
||||
if (format === undefined) {
|
||||
return "";
|
||||
}
|
||||
return format;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { useTheme } from "@mui/material";
|
||||
import { Text } from "recharts";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions";
|
||||
/**
|
||||
* Custom tick component for rendering time with timezone.
|
||||
*
|
||||
@@ -15,9 +15,10 @@ import { Box, Stack, Typography } from "@mui/material";
|
||||
* @param {number} props.index - The index of the tick.
|
||||
* @returns {JSX.Element} The rendered tick component.
|
||||
*/
|
||||
export const TzTick = ({ x, y, payload, index }) => {
|
||||
export const TzTick = ({ x, y, payload, index, dateRange }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const format = tickDateFormatLookup(dateRange);
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
@@ -27,7 +28,7 @@ export const TzTick = ({ x, y, payload, index }) => {
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -37,6 +38,7 @@ TzTick.propTypes = {
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -109,9 +111,12 @@ export const InfrastructureTooltip = ({
|
||||
yIdx = -1,
|
||||
yLabel,
|
||||
dotColor,
|
||||
dateRange,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
|
||||
const format = tooltipDateFormatLookup(dateRange);
|
||||
if (active && payload && payload.length) {
|
||||
const [hardwareType, metric] = yKey.split(".");
|
||||
return (
|
||||
@@ -133,7 +138,7 @@ export const InfrastructureTooltip = ({
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
{formatDateWithTz(label, format, uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
@@ -185,11 +190,20 @@ InfrastructureTooltip.propTypes = {
|
||||
yIdx: PropTypes.number,
|
||||
yLabel: PropTypes.string,
|
||||
dotColor: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
export const TemperatureTooltip = ({ active, payload, label, keys, dotColor }) => {
|
||||
export const TemperatureTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
keys,
|
||||
dotColor,
|
||||
dateRange,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
const format = tooltipDateFormatLookup(dateRange);
|
||||
const formatCoreKey = (key) => {
|
||||
return key.replace(/^core(\d+)$/, "Core $1");
|
||||
};
|
||||
@@ -213,7 +227,7 @@ export const TemperatureTooltip = ({ active, payload, label, keys, dotColor }) =
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
{formatDateWithTz(label, format, uiTimezone)}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="column">
|
||||
@@ -273,4 +287,6 @@ TemperatureTooltip.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
dotColor: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -7,11 +7,10 @@ import { credentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import BasicTable from "../../BasicTable";
|
||||
import Select from "../../Inputs/Select";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
|
||||
import DataTable from "../../Table/";
|
||||
/**
|
||||
* TeamPanel component manages the organization and team members,
|
||||
* providing functionalities like renaming the organization, managing team members,
|
||||
@@ -21,34 +20,47 @@ import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
*/
|
||||
|
||||
const TeamPanel = () => {
|
||||
const roleMap = {
|
||||
superadmin: "Super admin",
|
||||
admin: "Admin",
|
||||
user: "Team member",
|
||||
demo: "Demo User",
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const SPACING_GAP = theme.spacing(12);
|
||||
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
//TODO
|
||||
// const [orgStates, setOrgStates] = useState({
|
||||
// name: "Bluewave Labs",
|
||||
// isEdit: false,
|
||||
// });
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [toInvite, setToInvite] = useState({
|
||||
email: "",
|
||||
role: ["0"],
|
||||
});
|
||||
const [tableData, setTableData] = useState({});
|
||||
const [data, setData] = useState([]);
|
||||
const [members, setMembers] = useState([]);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
content: "Name",
|
||||
render: (row) => {
|
||||
return (
|
||||
<Stack>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{row.firstName + " " + row.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Created {new Date(row.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ id: "email", content: "Email", render: (row) => row.email },
|
||||
{
|
||||
id: "role",
|
||||
content: "Role",
|
||||
render: (row) => row.role,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
try {
|
||||
@@ -67,6 +79,12 @@ const TeamPanel = () => {
|
||||
}, [authToken]);
|
||||
|
||||
useEffect(() => {
|
||||
const ROLE_MAP = {
|
||||
superadmin: "Super admin",
|
||||
admin: "Admin",
|
||||
user: "Team member",
|
||||
demo: "Demo User",
|
||||
};
|
||||
let team = members;
|
||||
if (filter !== "all")
|
||||
team = members.filter((member) => {
|
||||
@@ -76,42 +94,14 @@ const TeamPanel = () => {
|
||||
return member.role.includes(filter);
|
||||
});
|
||||
|
||||
const data = {
|
||||
cols: [
|
||||
{ id: 1, name: "NAME" },
|
||||
{ id: 2, name: "EMAIL" },
|
||||
{ id: 3, name: "ROLE" },
|
||||
],
|
||||
rows: team?.map((member, idx) => {
|
||||
const roles = member.role.map((role) => roleMap[role]).join(",");
|
||||
return {
|
||||
id: member._id,
|
||||
data: [
|
||||
{
|
||||
id: idx,
|
||||
data: (
|
||||
<Stack>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{member.firstName + " " + member.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Created {new Date(member.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: idx + 1, data: member.email },
|
||||
{
|
||||
id: idx + 2,
|
||||
data: roles,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
team = team.map((member) => ({
|
||||
...member,
|
||||
id: member._id,
|
||||
role: member.role.map((role) => ROLE_MAP[role]).join(","),
|
||||
}));
|
||||
setData(team);
|
||||
}, [filter, members]);
|
||||
|
||||
setTableData(data);
|
||||
}, [filter, members, roleMap, theme]);
|
||||
useEffect(() => {
|
||||
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
|
||||
}, [errors, toInvite.email]);
|
||||
@@ -248,12 +238,11 @@ const TeamPanel = () => {
|
||||
Invite a team member
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
<BasicTable
|
||||
data={tableData}
|
||||
paginated={false}
|
||||
reversed={true}
|
||||
table={"team"}
|
||||
emptyMessage={"There are no team members with this role"}
|
||||
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{ emptyView: "There are no team members with this role" }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import LeftArrowDouble from "../../../../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../../../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../../../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../../../../assets/icons/right-arrow.svg?react";
|
||||
import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack, TablePagination, Typography } from "@mui/material";
|
||||
import { TablePaginationActions } from "./Actions";
|
||||
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||
import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
|
||||
|
||||
Pagination.propTypes = {
|
||||
monitorCount: PropTypes.number.isRequired, // Total number of items for pagination.
|
||||
113
Client/src/Components/Table/index.jsx
Normal file
113
Client/src/Components/Table/index.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Header
|
||||
* @property {number|string} id - The unique identifier for the header.
|
||||
* @property {React.ReactNode} content - The content to display in the header cell.
|
||||
* @property {Function} render - A function to render the cell content for a given row.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {Function} onRowClick - A function to be called when a row is clicked, receiving the row data as an argument.
|
||||
* @property {Object} rowSX - Style object for the table row.
|
||||
*/
|
||||
|
||||
/**
|
||||
* DataTable component renders a table with headers and data.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Header[]} props.headers - An array of header objects, each containing an `id`, `content`, and `render` function.
|
||||
* @param {Array} props.data - An array of data objects, each representing a row.
|
||||
* @returns {JSX.Element} The rendered table component.
|
||||
*/
|
||||
|
||||
const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => {
|
||||
const theme = useTheme();
|
||||
if ((headers?.length ?? 0) === 0) {
|
||||
return "No data";
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader>
|
||||
<TableHead sx={{ backgroundColor: theme.palette.background.accent }}>
|
||||
<TableRow>
|
||||
{headers.map((header, index) => (
|
||||
<TableCell
|
||||
key={header.id}
|
||||
align={index === 0 ? "left" : "center"}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}}
|
||||
>
|
||||
{header.content}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(data?.length ?? 0) === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={headers.length}
|
||||
align="center"
|
||||
>
|
||||
{config.emptyView}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
sx={config?.rowSX ?? {}}
|
||||
onClick={() => config?.onRowClick(row)}
|
||||
>
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
>
|
||||
{header.render(row)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
DataTable.propTypes = {
|
||||
headers: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
content: PropTypes.node.isRequired,
|
||||
render: PropTypes.func.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
config: PropTypes.shape({
|
||||
onRowClick: PropTypes.func.isRequired,
|
||||
rowSX: PropTypes.object,
|
||||
emptyView: PropTypes.node,
|
||||
}),
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
@@ -1,17 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { Pagination, PaginationItem, Typography, Box } from "@mui/material";
|
||||
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
@@ -27,7 +15,7 @@ import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?re
|
||||
import { HttpStatusLabel } from "../../../Components/HttpStatusLabel";
|
||||
import { Empty } from "./Empty/Empty";
|
||||
import { IncidentSkeleton } from "./Skeleton/Skeleton";
|
||||
|
||||
import DataTable from "../../../Components/Table";
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
@@ -106,6 +94,46 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: "Monitor Name",
|
||||
render: (row) => monitors[row.monitorId]?.name ?? "N/A",
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "up" : "down";
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "dateTime",
|
||||
content: "Date & Time",
|
||||
render: (row) => {
|
||||
const formattedDate = formatDateWithTz(
|
||||
row.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return formattedDate;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: "Status Code",
|
||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{ id: "message", content: "Message", render: (row) => row.message },
|
||||
];
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
@@ -166,47 +194,11 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Status Code</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
const formattedDate = formatDateWithTz(
|
||||
check.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formattedDate}</TableCell>
|
||||
<TableCell>
|
||||
<HttpStatusLabel status={check.statusCode} />
|
||||
</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={checks}
|
||||
/>
|
||||
|
||||
{paginationComponent}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -13,9 +13,8 @@ import { useNavigate } from "react-router-dom";
|
||||
import Empty from "./empty";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
|
||||
import { TzTick, PercentTick } from "../../../Components/Charts/Utils/chartUtils";
|
||||
import {
|
||||
TzTick,
|
||||
PercentTick,
|
||||
InfrastructureTooltip,
|
||||
TemperatureTooltip,
|
||||
} from "../../../Components/Charts/Utils/chartUtils";
|
||||
@@ -414,12 +413,13 @@ const InfrastructureDetails = () => {
|
||||
yLabel: "Memory usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.primary.main}
|
||||
yKey={"avgMemoryUsage"}
|
||||
yLabel={"Memory usage"}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -433,12 +433,13 @@ const InfrastructureDetails = () => {
|
||||
yLabel: "CPU usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.success.main}
|
||||
yKey={"avgCpuUsage"}
|
||||
yLabel={"CPU usage"}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -450,7 +451,7 @@ const InfrastructureDetails = () => {
|
||||
gradientStartColor: theme.palette.error.main,
|
||||
heading: "CPU Temperature",
|
||||
yLabel: "Temperature",
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
yDomain: [
|
||||
0,
|
||||
Math.max(
|
||||
@@ -462,6 +463,7 @@ const InfrastructureDetails = () => {
|
||||
<TemperatureTooltip
|
||||
keys={tempKeys}
|
||||
dotColor={theme.palette.error.main}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -476,13 +478,14 @@ const InfrastructureDetails = () => {
|
||||
yLabel: "Disk Usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.warning.main}
|
||||
yKey={`disks.usagePercent`}
|
||||
yLabel={"Disc usage"}
|
||||
yIdx={idx}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
})) || []),
|
||||
|
||||
@@ -8,23 +8,12 @@ import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
// import GearIcon from "../../Assets/icons/settings-bold.svg?react";
|
||||
import CPUChipIcon from "../../assets/icons/cpu-chip.svg?react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import DataTable from "../../Components/Table";
|
||||
import { Box, Button, IconButton, Stack } from "@mui/material";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { Heading } from "../../Components/Heading";
|
||||
import { Pagination } from "./components/TablePagination";
|
||||
import { Pagination } from "../../Components/Table/TablePagination/index.jsx";
|
||||
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
|
||||
import { networkService } from "../../Utils/NetworkService.js";
|
||||
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
|
||||
@@ -32,42 +21,8 @@ import Host from "../Uptime/Home/host.jsx";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
|
||||
import { InfrastructureMenu } from "./components/Menu";
|
||||
|
||||
const columns = [
|
||||
{ label: "Host" },
|
||||
{ label: "Status" },
|
||||
{ label: "Frequency" },
|
||||
{ label: "CPU" },
|
||||
{ label: "Mem" },
|
||||
{ label: "Disk" },
|
||||
{ label: "Actions" },
|
||||
];
|
||||
|
||||
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
|
||||
|
||||
/* TODO
|
||||
Create reusable table component.
|
||||
It should receive as a parameter the following object:
|
||||
tableData = [
|
||||
columns = [
|
||||
{
|
||||
id: example,
|
||||
label: Example Extendable,
|
||||
align: "center" | "left" (default)
|
||||
}
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
**Number of keys will be equal to number of columns**
|
||||
key1: string,
|
||||
key2: number,
|
||||
key3: Component
|
||||
}
|
||||
]
|
||||
]
|
||||
Apply to Monitor Table, and Account/Team.
|
||||
Analyze existing BasicTable
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is the Infrastructure monitoring page. This is a work in progress
|
||||
*
|
||||
@@ -139,6 +94,71 @@ function Infrastructure() {
|
||||
fetchMonitors();
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "host",
|
||||
content: "Host",
|
||||
render: (row) => (
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "frequency",
|
||||
content: "Frequency",
|
||||
render: (row) => (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<CPUChipIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: "cpu", content: "CPU", render: (row) => <CustomGauge progress={row.cpu} /> },
|
||||
{ id: "mem", content: "Mem", render: (row) => <CustomGauge progress={row.mem} /> },
|
||||
{ id: "disk", content: "Disk", render: (row) => <CustomGauge progress={row.disk} /> },
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
render: (row) => (
|
||||
<IconButton
|
||||
sx={{
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const monitorsAsRows = monitors.map((monitor) => {
|
||||
const processor =
|
||||
((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz";
|
||||
@@ -237,103 +257,20 @@ function Infrastructure() {
|
||||
{totalMonitors}
|
||||
</Box>
|
||||
</Stack>
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader>
|
||||
<TableHead sx={{ backgroundColor: theme.palette.background.accent }}>
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
align={index === 0 ? "left" : "center"}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitorsAsRows.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => openDetails(row.id)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* TODO iterate over column and get column id, applying row[column.id] */}
|
||||
<TableCell>
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/* Use capitalize inside of Status Label */
|
||||
/* Update component so we don't need to pass text and status separately*/
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<CPUChipIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.cpu} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.mem} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.disk} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{/* Get ActionsMenu from Monitor Table and create a component */}
|
||||
<IconButton
|
||||
sx={{
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
{/* <GearIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/> */}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<DataTable
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
}}
|
||||
headers={headers}
|
||||
data={monitorsAsRows}
|
||||
/>
|
||||
<Pagination
|
||||
monitorCount={totalMonitors}
|
||||
page={page}
|
||||
|
||||
@@ -308,7 +308,9 @@ const DetailsPage = () => {
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? hoveredUptimeData.totalChecks
|
||||
: (monitor.stats?.upChecksAggregate?.totalChecks ?? 0)}
|
||||
: (monitor.stats?.upChecks?.reduce((count, checkGroup) => {
|
||||
return count + checkGroup.totalChecks;
|
||||
}, 0) ?? 0)}
|
||||
</Typography>
|
||||
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
|
||||
<Typography
|
||||
@@ -364,7 +366,9 @@ const DetailsPage = () => {
|
||||
<Typography component="span">
|
||||
{hoveredIncidentsData !== null
|
||||
? hoveredIncidentsData.totalChecks
|
||||
: (monitor.stats?.downChecksAggregate?.totalChecks ?? 0)}
|
||||
: (monitor.stats?.downChecks?.reduce((count, checkGroup) => {
|
||||
return count + checkGroup.totalChecks;
|
||||
}, 0) ?? 0)}
|
||||
</Typography>
|
||||
{hoveredIncidentsData !== null &&
|
||||
hoveredIncidentsData.time !== null && (
|
||||
@@ -410,7 +414,10 @@ const DetailsPage = () => {
|
||||
</IconBox>
|
||||
<Typography component="h2">Response Times</Typography>
|
||||
</Stack>
|
||||
<MonitorDetailsAreaChart checks={monitor?.stats?.groupChecks ?? []} />
|
||||
<MonitorDetailsAreaChart
|
||||
checks={monitor?.stats?.groupChecks ?? []}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
gap={theme.spacing(8)}
|
||||
|
||||
@@ -1,32 +1,47 @@
|
||||
import { Skeleton, TableCell, TableRow } from "@mui/material";
|
||||
import { Skeleton } from "@mui/material";
|
||||
import DataTable from "../../../../../Components/Table";
|
||||
const ROWS_NUMBER = 7;
|
||||
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
|
||||
|
||||
const TableBodySkeleton = () => {
|
||||
const TableSkeleton = () => {
|
||||
/* TODO Skeleton does not follow light and dark theme */
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
|
||||
content: "Host",
|
||||
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "responseTime",
|
||||
content: "Response Time",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: "Type",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{ROWS_ARRAY.map((row) => (
|
||||
<TableRow key={row}>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={ROWS_ARRAY}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { TableBodySkeleton };
|
||||
export { TableSkeleton };
|
||||
|
||||
@@ -10,27 +10,18 @@ import { logger } from "../../../../Utils/Logger";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../../../main";
|
||||
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { Box, CircularProgress } from "@mui/material";
|
||||
import ActionsMenu from "../actionsMenu";
|
||||
import Host from "../host";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import { TableBodySkeleton } from "./Skeleton";
|
||||
import { TableSkeleton } from "./Skeleton";
|
||||
import BarChart from "../../../../Components/Charts/BarChart";
|
||||
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
|
||||
import { Pagination } from "../../../Infrastructure/components/TablePagination";
|
||||
import { Pagination } from "../../../../Components/Table/TablePagination";
|
||||
import DataTable from "../../../../Components/Table";
|
||||
|
||||
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePause }) => {
|
||||
const theme = useTheme();
|
||||
@@ -46,7 +37,95 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [sort, setSort] = useState({});
|
||||
const [data, setData] = useState([]);
|
||||
const prevFilter = useRef(filter);
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
content: (
|
||||
<Box onClick={() => handleSort("name")}>
|
||||
Host
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
),
|
||||
render: (row) => (
|
||||
<Host
|
||||
key={row._id}
|
||||
url={row.url}
|
||||
title={row.title}
|
||||
percentageColor={row.percentageColor}
|
||||
percentage={row.percentage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: (
|
||||
<Box
|
||||
width="max-content"
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
),
|
||||
render: (row) => {
|
||||
const status = determineState(row.monitor);
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "responseTime",
|
||||
content: "Response Time",
|
||||
render: (row) => <BarChart checks={row.monitor.checks.slice().reverse()} />,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: "Type",
|
||||
render: (row) => (
|
||||
<span style={{ textTransform: "uppercase" }}>{row.monitor.type}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
render: (row) => (
|
||||
<ActionsMenu
|
||||
monitor={row.monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateRowCallback={handleRowUpdate}
|
||||
pauseCallback={handlePause}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleRowUpdate = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
@@ -144,7 +223,41 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
};
|
||||
/* TODO Apply component basic table? */
|
||||
|
||||
useEffect(() => {
|
||||
const mappedMonitors = monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
return {
|
||||
id: monitor._id,
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
monitor: monitor,
|
||||
};
|
||||
});
|
||||
setData(mappedMonitors);
|
||||
}, [monitors, theme]);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
{isSearching && (
|
||||
@@ -177,141 +290,37 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Host
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */}
|
||||
{monitors.length > 0 ? (
|
||||
monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status: determineState(monitor),
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
filter: "brightness(.75)",
|
||||
opacity: 0.75,
|
||||
transition: "filter 0.3s ease, opacity 0.3s ease",
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/uptime/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
url={params.url}
|
||||
title={params.title}
|
||||
percentageColor={params.percentageColor}
|
||||
percentage={params.percentage}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateRowCallback={handleRowUpdate}
|
||||
pauseCallback={handlePause}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBodySkeleton />
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{/*
|
||||
This is the original SX for the row, doesn't match infrastructure table
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
filter: "brightness(.75)",
|
||||
opacity: 0.75,
|
||||
transition: "filter 0.3s ease, opacity 0.3s ease",
|
||||
},
|
||||
},
|
||||
*/}
|
||||
{monitors.length > 0 ? (
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
},
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TableSkeleton />
|
||||
)}
|
||||
<Pagination
|
||||
monitorCount={monitorCount}
|
||||
page={page}
|
||||
|
||||
@@ -8,7 +8,10 @@ import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUt
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import {
|
||||
buildUptimeDetailsPipeline,
|
||||
buildHardwareDetailsPipeline,
|
||||
} from "./monitorModuleQueries.js";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -336,272 +339,9 @@ const getUptimeDetailsById = async (req) => {
|
||||
};
|
||||
|
||||
const dateString = formatLookup[dateRange];
|
||||
const monitorData = await Check.aggregate([
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
aggregateData: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
firstCheck: {
|
||||
$first: "$$ROOT",
|
||||
},
|
||||
lastCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
uptimeDuration: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
lastFalseCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupChecks: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
groupAggregate: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecksAggregate: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
downChecksAggregate: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
downChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
avgResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.avgResponseTime", 0],
|
||||
},
|
||||
totalChecks: {
|
||||
$arrayElemAt: ["$aggregateData.totalChecks", 0],
|
||||
},
|
||||
latestResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
|
||||
},
|
||||
timeSinceLastCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastCheck: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
timeSinceLastFalseCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastFalseCheck: {
|
||||
$arrayElemAt: ["$uptimeDuration.lastFalseCheck", 0],
|
||||
},
|
||||
firstCheck: {
|
||||
$arrayElemAt: ["$aggregateData.firstCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastFalseCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastFalseCheck.createdAt"],
|
||||
},
|
||||
{
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$firstCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$firstCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
groupChecks: "$groupChecks",
|
||||
groupAggregate: {
|
||||
$arrayElemAt: ["$groupAggregate", 0],
|
||||
},
|
||||
upChecksAggregate: {
|
||||
$arrayElemAt: ["$upChecksAggregate", 0],
|
||||
},
|
||||
upChecks: "$upChecks",
|
||||
downChecksAggregate: {
|
||||
$arrayElemAt: ["$downChecksAggregate", 0],
|
||||
},
|
||||
downChecks: "$downChecks",
|
||||
},
|
||||
},
|
||||
]);
|
||||
const monitorData = await Check.aggregate(
|
||||
buildUptimeDetailsPipeline(monitor, dates, dateString)
|
||||
);
|
||||
|
||||
const normalizedGroupChecks = NormalizeDataUptimeDetails(
|
||||
monitorData[0].groupChecks,
|
||||
@@ -715,261 +455,9 @@ const getHardwareDetailsById = async (req) => {
|
||||
month: "%Y-%m-%dT00:00:00Z",
|
||||
};
|
||||
const dateString = formatLookup[dateRange];
|
||||
const hardwareStats = await HardwareCheck.aggregate([
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
aggregateData: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
latestCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
checks: [
|
||||
{
|
||||
$limit: 1,
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
diskCount: {
|
||||
$size: "$disk",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "hardwarechecks",
|
||||
let: {
|
||||
diskCount: "$diskCount",
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ["$monitorId", monitor._id] },
|
||||
{ $gte: ["$createdAt", dates.start] },
|
||||
{ $lte: ["$createdAt", dates.end] },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgCpuUsage: {
|
||||
$avg: "$cpu.usage_percent",
|
||||
},
|
||||
avgMemoryUsage: {
|
||||
$avg: "$memory.usage_percent",
|
||||
},
|
||||
avgTemperatures: {
|
||||
$push: {
|
||||
$ifNull: ["$cpu.temperature", [0]],
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
$push: "$disk",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
avgCpuUsage: 1,
|
||||
avgMemoryUsage: 1,
|
||||
avgTemperature: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [
|
||||
0,
|
||||
{
|
||||
$size: {
|
||||
// Handle null temperatures array
|
||||
$ifNull: [
|
||||
{ $arrayElemAt: ["$avgTemperatures", 0] },
|
||||
[0], // Default to single-element array if null
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
as: "index",
|
||||
in: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$avgTemperatures",
|
||||
as: "tempArray",
|
||||
in: {
|
||||
$ifNull: [
|
||||
{ $arrayElemAt: ["$$tempArray", "$$index"] },
|
||||
0, // Default to 0 if element is null
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, "$$diskCount"],
|
||||
},
|
||||
as: "diskIndex",
|
||||
in: {
|
||||
name: {
|
||||
$concat: [
|
||||
"disk",
|
||||
{
|
||||
$toString: "$$diskIndex",
|
||||
},
|
||||
],
|
||||
},
|
||||
readSpeed: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.read_speed_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
writeSpeed: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.write_speed_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalBytes: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.total_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
freeBytes: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.free_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
usagePercent: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.usage_percent",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
as: "hourlyStats",
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: "$hourlyStats",
|
||||
},
|
||||
{
|
||||
$replaceRoot: {
|
||||
newRoot: "$hourlyStats",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
aggregateData: {
|
||||
$arrayElemAt: ["$aggregateData", 0],
|
||||
},
|
||||
upChecks: {
|
||||
$arrayElemAt: ["$upChecks", 0],
|
||||
},
|
||||
checks: {
|
||||
$sortArray: {
|
||||
input: "$checks",
|
||||
sortBy: { _id: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
const hardwareStats = await HardwareCheck.aggregate(
|
||||
buildHardwareDetailsPipeline(monitor, dates, dateString)
|
||||
);
|
||||
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
|
||||
525
Server/db/mongo/modules/monitorModuleQueries.js
Normal file
525
Server/db/mongo/modules/monitorModuleQueries.js
Normal file
@@ -0,0 +1,525 @@
|
||||
const buildUptimeDetailsPipeline = (monitor, dates, dateString) => {
|
||||
return [
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
aggregateData: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
firstCheck: {
|
||||
$first: "$$ROOT",
|
||||
},
|
||||
lastCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
uptimeDuration: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
lastFalseCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupChecks: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
groupAggregate: [
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecksAggregate: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
downChecksAggregate: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
downChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: false,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
avgResponseTime: {
|
||||
$avg: "$responseTime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
avgResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.avgResponseTime", 0],
|
||||
},
|
||||
totalChecks: {
|
||||
$arrayElemAt: ["$aggregateData.totalChecks", 0],
|
||||
},
|
||||
latestResponseTime: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
|
||||
},
|
||||
timeSinceLastCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastCheck: {
|
||||
$arrayElemAt: ["$aggregateData.lastCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
timeSinceLastFalseCheck: {
|
||||
$let: {
|
||||
vars: {
|
||||
lastFalseCheck: {
|
||||
$arrayElemAt: ["$uptimeDuration.lastFalseCheck", 0],
|
||||
},
|
||||
firstCheck: {
|
||||
$arrayElemAt: ["$aggregateData.firstCheck", 0],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$lastFalseCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$lastFalseCheck.createdAt"],
|
||||
},
|
||||
{
|
||||
$cond: [
|
||||
{
|
||||
$ifNull: ["$$firstCheck", false],
|
||||
},
|
||||
{
|
||||
$subtract: [new Date(), "$$firstCheck.createdAt"],
|
||||
},
|
||||
0,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
groupChecks: "$groupChecks",
|
||||
groupAggregate: {
|
||||
$arrayElemAt: ["$groupAggregate", 0],
|
||||
},
|
||||
upChecksAggregate: {
|
||||
$arrayElemAt: ["$upChecksAggregate", 0],
|
||||
},
|
||||
upChecks: "$upChecks",
|
||||
downChecksAggregate: {
|
||||
$arrayElemAt: ["$downChecksAggregate", 0],
|
||||
},
|
||||
downChecks: "$downChecks",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
return [
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
aggregateData: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
latestCheck: {
|
||||
$last: "$$ROOT",
|
||||
},
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
upChecks: [
|
||||
{
|
||||
$match: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalChecks: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
checks: [
|
||||
{
|
||||
$limit: 1,
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
diskCount: {
|
||||
$size: "$disk",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "hardwarechecks",
|
||||
let: {
|
||||
diskCount: "$diskCount",
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ["$monitorId", monitor._id] },
|
||||
{ $gte: ["$createdAt", dates.start] },
|
||||
{ $lte: ["$createdAt", dates.end] },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: {
|
||||
format: dateString,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
avgCpuUsage: {
|
||||
$avg: "$cpu.usage_percent",
|
||||
},
|
||||
avgMemoryUsage: {
|
||||
$avg: "$memory.usage_percent",
|
||||
},
|
||||
avgTemperatures: {
|
||||
$push: {
|
||||
$ifNull: ["$cpu.temperature", [0]],
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
$push: "$disk",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
avgCpuUsage: 1,
|
||||
avgMemoryUsage: 1,
|
||||
avgTemperature: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [
|
||||
0,
|
||||
{
|
||||
$size: {
|
||||
// Handle null temperatures array
|
||||
$ifNull: [
|
||||
{ $arrayElemAt: ["$avgTemperatures", 0] },
|
||||
[0], // Default to single-element array if null
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
as: "index",
|
||||
in: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$avgTemperatures",
|
||||
as: "tempArray",
|
||||
in: {
|
||||
$ifNull: [
|
||||
{ $arrayElemAt: ["$$tempArray", "$$index"] },
|
||||
0, // Default to 0 if element is null
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, "$$diskCount"],
|
||||
},
|
||||
as: "diskIndex",
|
||||
in: {
|
||||
name: {
|
||||
$concat: [
|
||||
"disk",
|
||||
{
|
||||
$toString: "$$diskIndex",
|
||||
},
|
||||
],
|
||||
},
|
||||
readSpeed: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.read_speed_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
writeSpeed: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.write_speed_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalBytes: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.total_bytes",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
freeBytes: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: ["$$diskArray.free_bytes", "$$diskIndex"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
usagePercent: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$disks",
|
||||
as: "diskArray",
|
||||
in: {
|
||||
$arrayElemAt: [
|
||||
"$$diskArray.usage_percent",
|
||||
"$$diskIndex",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
as: "hourlyStats",
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: "$hourlyStats",
|
||||
},
|
||||
{
|
||||
$replaceRoot: {
|
||||
newRoot: "$hourlyStats",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
aggregateData: {
|
||||
$arrayElemAt: ["$aggregateData", 0],
|
||||
},
|
||||
upChecks: {
|
||||
$arrayElemAt: ["$upChecks", 0],
|
||||
},
|
||||
checks: {
|
||||
$sortArray: {
|
||||
input: "$checks",
|
||||
sortBy: { _id: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export { buildUptimeDetailsPipeline, buildHardwareDetailsPipeline };
|
||||
Reference in New Issue
Block a user