Merge branch 'develop' into fix/fe/uptime-home

This commit is contained in:
Alex Holliday
2025-01-07 12:05:19 -08:00
17 changed files with 1108 additions and 1450 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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