diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css deleted file mode 100644 index 3c1b0f2cd..000000000 --- a/Client/src/Components/BasicTable/index.css +++ /dev/null @@ -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); -} diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx deleted file mode 100644 index cfc3030b0..000000000 --- a/Client/src/Components/BasicTable/index.jsx +++ /dev/null @@ -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 ( - - - - - - - ); -}; - -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:
Data for Row 1 Col 1
}, - * { id: 2, data:
Data for Row 1 Col 2
}, - * { id: 3, data:
Data for Row 1 Col 3
}, - * { id: 4, data:
Data for Row 1 Col 4
}, - * ], - * }, - * { - * id: 2, - * data: [ - * { id: 5, data:
Data for Row 2 Col 1
}, - * { id: 6, data:
Data for Row 2 Col 2
}, - * { id: 7, data:
Data for Row 2 Col 3
}, - * { id: 8, data:
Data for Row 2 Col 4
}, - * ], - * }, - * ], - * }; - * - * - */ - -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
No data
; - } - - /** - * 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 ( - <> - - - - - {data.cols.map((col) => ( - {col.name} - ))} - - - - {displayData.map((row) => { - return ( - - {row.data.map((cell) => { - return {cell.data}; - })} - - ); - })} - {displayData.length === 0 && ( - - - {emptyMessage} - - - )} - -
-
- {paginated && ( - - - Showing {getRange()} of {data.rows.length} monitor(s) - - - `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, - }, - }} - /> - - )} - - ); -}; - -BasicTable.propTypes = { - data: PropTypes.object.isRequired, - paginated: PropTypes.bool, - reversed: PropTypes.bool, - rowPerPage: PropTypes.number, - table: PropTypes.string, - emptyMessage: PropTypes.string, -}; - -export default BasicTable; diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 2fefd3daa..91cb1bf22 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -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)} { +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 ( { fontSize={11} fontWeight={400} > - {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} + {formatDateWithTz(payload?.value, format, uiTimezone)} ); }; @@ -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 }) => { } - minTickGap={0} + tick={} axisLine={false} tickLine={false} height={20} - interval="equidistantPreserveStart" /> } + content={} wrapperStyle={{ pointerEvents: "none" }} /> { MonitorDetailsAreaChart.propTypes = { checks: PropTypes.array, + dateRange: PropTypes.string, }; export default MonitorDetailsAreaChart; diff --git a/Client/src/Components/Charts/Utils/chartUtilFunctions.js b/Client/src/Components/Charts/Utils/chartUtilFunctions.js new file mode 100644 index 000000000..70f5ac9c3 --- /dev/null +++ b/Client/src/Components/Charts/Utils/chartUtilFunctions.js @@ -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; +}; diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx index 1d82f10b4..1776c75d9 100644 --- a/Client/src/Components/Charts/Utils/chartUtils.jsx +++ b/Client/src/Components/Charts/Utils/chartUtils.jsx @@ -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 ( { fontSize={11} fontWeight={400} > - {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} + {formatDateWithTz(payload?.value, format, uiTimezone)} ); }; @@ -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)} { +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)} @@ -273,4 +287,6 @@ TemperatureTooltip.propTypes = { PropTypes.string, PropTypes.number, ]), + dotColor: PropTypes.string, + dateRange: PropTypes.string, }; diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index bbc6c4305..73d63e4b9 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -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 ( + + + {row.firstName + " " + row.lastName} + + + Created {new Date(row.createdAt).toLocaleDateString()} + + + ); + }, + }, + { 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: ( - - - {member.firstName + " " + member.lastName} - - - Created {new Date(member.createdAt).toLocaleDateString()} - - - ), - }, - { 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 - diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx b/Client/src/Components/Table/TablePagination/Actions/index.jsx similarity index 85% rename from Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx rename to Client/src/Components/Table/TablePagination/Actions/index.jsx index c684da145..c607fcb8b 100644 --- a/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx +++ b/Client/src/Components/Table/TablePagination/Actions/index.jsx @@ -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, diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx similarity index 97% rename from Client/src/Pages/Infrastructure/components/TablePagination/index.jsx rename to Client/src/Components/Table/TablePagination/index.jsx index bb618add8..4435454bc 100644 --- a/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx +++ b/Client/src/Components/Table/TablePagination/index.jsx @@ -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. diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx new file mode 100644 index 000000000..3cef178f1 --- /dev/null +++ b/Client/src/Components/Table/index.jsx @@ -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 ( + + + + + {headers.map((header, index) => ( + + {header.content} + + ))} + + + + {(data?.length ?? 0) === 0 ? ( + + + {config.emptyView} + + + ) : ( + data.map((row) => { + return ( + config?.onRowClick(row)} + > + {headers.map((header, index) => { + return ( + + {header.render(row)} + + ); + })} + + ); + }) + )} + +
+
+ ); +}; + +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; diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 1d0be7666..b4ef9fdb8 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -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 ( + + ); + }, + }, + { + 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) => , + }, + { id: "message", content: "Message", render: (row) => row.message }, + ]; + let paginationComponent = <>; if (checksCount > paginationController.rowsPerPage) { paginationComponent = ( @@ -166,47 +194,11 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
) : ( <> - - - - - Monitor Name - Status - Date & Time - Status Code - Message - - - - {checks.map((check) => { - const status = check.status === true ? "up" : "down"; - const formattedDate = formatDateWithTz( - check.createdAt, - "YYYY-MM-DD HH:mm:ss A", - uiTimezone - ); - - return ( - - {monitors[check.monitorId]?.name} - - - - {formattedDate} - - - - {check.message} - - ); - })} - -
-
+ + {paginationComponent} )} diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index 29eddb9fc..5a0f59d5c 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -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: , - xTick: , + xTick: , toolTip: ( ), }, @@ -433,12 +433,13 @@ const InfrastructureDetails = () => { yLabel: "CPU usage", yDomain: [0, 1], yTick: , - xTick: , + xTick: , toolTip: ( ), }, @@ -450,7 +451,7 @@ const InfrastructureDetails = () => { gradientStartColor: theme.palette.error.main, heading: "CPU Temperature", yLabel: "Temperature", - xTick: , + xTick: , yDomain: [ 0, Math.max( @@ -462,6 +463,7 @@ const InfrastructureDetails = () => { ), }, @@ -476,13 +478,14 @@ const InfrastructureDetails = () => { yLabel: "Disk Usage", yDomain: [0, 1], yTick: , - xTick: , + xTick: , toolTip: ( ), })) || []), diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index aba4369e6..be7d7e9ed 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -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) => ( + + ), + }, + { + id: "status", + content: "Status", + render: (row) => ( + + ), + }, + { + id: "frequency", + content: "Frequency", + render: (row) => ( + + + {row.processor} + + ), + }, + { id: "cpu", content: "CPU", render: (row) => }, + { id: "mem", content: "Mem", render: (row) => }, + { id: "disk", content: "Disk", render: (row) => }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + + + ), + }, + ]; + 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}
- - - - - {columns.map((column, index) => ( - - {column.label} - - ))} - - - - {monitorsAsRows.map((row) => { - return ( - openDetails(row.id)} - sx={{ - cursor: "pointer", - "&:hover": { - backgroundColor: theme.palette.background.accent, - }, - }} - > - {/* TODO iterate over column and get column id, applying row[column.id] */} - - - - - - - - - - {row.processor} - - - - - - - - - - - - - {/* Get ActionsMenu from Monitor Table and create a component */} - - - {/* */} - - - - ); - })} - -
-
+ + openDetails(row.id), + }} + headers={headers} + data={monitorsAsRows} + /> { {hoveredUptimeData !== null ? hoveredUptimeData.totalChecks - : (monitor.stats?.upChecksAggregate?.totalChecks ?? 0)} + : (monitor.stats?.upChecks?.reduce((count, checkGroup) => { + return count + checkGroup.totalChecks; + }, 0) ?? 0)} {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( { {hoveredIncidentsData !== null ? hoveredIncidentsData.totalChecks - : (monitor.stats?.downChecksAggregate?.totalChecks ?? 0)} + : (monitor.stats?.downChecks?.reduce((count, checkGroup) => { + return count + checkGroup.totalChecks; + }, 0) ?? 0)} {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && ( @@ -410,7 +414,10 @@ const DetailsPage = () => { Response Times - + i); -const TableBodySkeleton = () => { +const TableSkeleton = () => { /* TODO Skeleton does not follow light and dark theme */ + + const headers = [ + { + id: "name", + + content: "Host", + + render: () => , + }, + { + id: "status", + content: "Status", + render: () => , + }, + { + id: "responseTime", + content: "Response Time", + render: () => , + }, + { + id: "type", + content: "Type", + render: () => , + }, + { + id: "actions", + content: "Actions", + render: () => , + }, + ]; + return ( - <> - {ROWS_ARRAY.map((row) => ( - - - - - - - - - - - - - - - - - - ))} - + ); }; -export { TableBodySkeleton }; +export { TableSkeleton }; diff --git a/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx index a19da2a43..0aa0eef90 100644 --- a/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx @@ -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: ( + handleSort("name")}> + Host + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => ( + + ), + }, + { + id: "status", + content: ( + handleSort("status")} + > + {" "} + Status + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => { + const status = determineState(row.monitor); + return ( + + ); + }, + }, + { + id: "responseTime", + content: "Response Time", + render: (row) => , + }, + { + id: "type", + content: "Type", + render: (row) => ( + {row.monitor.type} + ), + }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + ), + }, + ]; 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 ( {isSearching && ( @@ -177,141 +290,37 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus )} - - - - - handleSort("name")} - > - - Host - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - - handleSort("status")} - > - {" "} - - {" "} - Status - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - - Response Time - Type - Actions - - - - {/* 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 ( - { - navigate(`/uptime/${monitor._id}`); - }} - > - - - - - - - - - - - {monitor.type} - - - - - - ); - }) - ) : ( - - )} - -
-
+ {/* + 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 ? ( + { + navigate(`/uptime/${row.id}`); + }, + emptyView: "No monitors found", + }} + /> + ) : ( + + )} { }; 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(), diff --git a/Server/db/mongo/modules/monitorModuleQueries.js b/Server/db/mongo/modules/monitorModuleQueries.js new file mode 100644 index 000000000..ccafbcbdc --- /dev/null +++ b/Server/db/mongo/modules/monitorModuleQueries.js @@ -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 };