From 35a9c081b5909a51385802b34e79c4259a6bedd1 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Tue, 1 Jul 2025 15:54:50 +0530 Subject: [PATCH 1/5] Feat: Status Boxes in Incidents Page --- .../Components/OptionsHeader/index.jsx | 4 +- .../Components/StatusBoxes/StatusBox.jsx | 116 ++++++++++++++++++ .../Components/StatusBoxes/index.jsx | 43 +++++++ .../Components/StatusBoxes/skeleton.jsx | 36 ++++++ client/src/Pages/Incidents/index.jsx | 4 + 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx create mode 100644 client/src/Pages/Incidents/Components/StatusBoxes/index.jsx create mode 100644 client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx index 457c6fd96..08a9bcf1c 100644 --- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx @@ -87,13 +87,13 @@ const OptionsHeader = ({ > {t("incidentsOptionsHeaderFilterCannotResolve")} - {/* */} + diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx new file mode 100644 index 000000000..ef7457a9e --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -0,0 +1,116 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Background from "../../../../assets/Images/background-grid.svg?react"; +import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined"; +import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined"; +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded"; + +const StatusBox = ({ title, value, status }) => { + const theme = useTheme(); + let sharedStyles = { + position: "absolute", + right: 8, + opacity: 0.5, + "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, + }; + + let color; + let icon; + if (status === "up") { + color = theme.palette.success.lowContrast; + icon = ( + + + + ); + } else if (status === "down") { + color = theme.palette.error.lowContrast; + icon = ( + + + + ); + } else if (status === "paused") { + color = theme.palette.warning.lowContrast; + icon = ( + + + + ); + } else { + color = theme.palette.accent.main; + icon = ( + + + + ); + } + + return ( + + + + + + + + {title} + + {icon} + + + {value} + + + # + + + + + ); +}; + +StatusBox.propTypes = { + title: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + status: PropTypes.string, +}; + +export default StatusBox; \ No newline at end of file diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..ae29f4a8f --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx @@ -0,0 +1,43 @@ +import PropTypes from "prop-types"; +import { Stack } from "@mui/material"; +import StatusBox from "./StatusBox"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const StatusBoxes = ({ shouldRender }) => { + const theme = useTheme(); + if (!shouldRender) return ; + return ( + + + + + + + ); +}; + +StatusBoxes.propTypes = { + shouldRender: PropTypes.bool, +}; + +export default StatusBoxes; \ No newline at end of file diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx new file mode 100644 index 000000000..b9aa016b7 --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx @@ -0,0 +1,36 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 09d4f0e25..8d668cb01 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -4,6 +4,7 @@ import Breadcrumbs from "../../Components/Breadcrumbs"; import GenericFallback from "../../Components/GenericFallback"; import IncidentTable from "./Components/IncidentTable"; import OptionsHeader from "./Components/OptionsHeader"; +import StatusBoxes from "./Components/StatusBoxes"; //Utils import { useTheme } from "@emotion/react"; @@ -62,6 +63,9 @@ const Incidents = () => { return ( + Date: Tue, 1 Jul 2025 17:55:50 +0530 Subject: [PATCH 2/5] Feat: Summary for Incidents --- .../Components/StatusBoxes/index.jsx | 20 +++++--- client/src/Pages/Incidents/index.jsx | 2 +- server/db/mongo/modules/checkModule.js | 23 +++++++++ server/db/mongo/modules/checkModuleQueries.js | 48 +++++++++++++++++++ 4 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 server/db/mongo/modules/checkModuleQueries.js diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx index ae29f4a8f..d44e00b22 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx @@ -4,9 +4,9 @@ import StatusBox from "./StatusBox"; import { useTheme } from "@emotion/react"; import SkeletonLayout from "./skeleton"; -const StatusBoxes = ({ shouldRender }) => { +const StatusBoxes = ({ isLoading, statusCounts }) => { const theme = useTheme(); - if (!shouldRender) return ; + if (isLoading) return ; return ( { > ); }; StatusBoxes.propTypes = { - shouldRender: PropTypes.bool, + isLoading: PropTypes.bool, + statusCounts: PropTypes.shape({ + total: PropTypes.number, + resolved: PropTypes.number, + cannotResolve: PropTypes.number, + down: PropTypes.number, + }), }; export default StatusBoxes; \ No newline at end of file diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 8d668cb01..708b443a5 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -64,7 +64,7 @@ const Incidents = () => { { } }; +/** + * Get checks and summary by team ID + * @async + * @param {string} teamId + * @returns {Promise} + * @throws {Error} + */ +const getChecksAndSummaryByTeamId = async ({ teamId }) => { + try { + const matchStage = { + teamId: new ObjectId(teamId), + } + const checks = await Check.aggregate(buildChecksAndSummaryByTeamIdPipeline({ matchStage })); + return checks[0].summary; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getChecksAndSummaryByTeamId"; + throw error; + } +}; + /** * Delete all checks for a monitor * @async @@ -387,6 +409,7 @@ export { getChecksByTeam, ackCheck, ackAllChecks, + getChecksAndSummaryByTeamId, deleteChecks, deleteChecksByTeamId, updateChecksTTL, diff --git a/server/db/mongo/modules/checkModuleQueries.js b/server/db/mongo/modules/checkModuleQueries.js new file mode 100644 index 000000000..097389a52 --- /dev/null +++ b/server/db/mongo/modules/checkModuleQueries.js @@ -0,0 +1,48 @@ +const buildChecksAndSummaryByTeamIdPipeline = ({ matchStage }) => { + return [ + { $match: matchStage }, + { + $facet: { + summary: [ + { + $group: { + _id: null, + totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } }, + resolvedChecks: { + $sum: { + $cond: [{ $eq: ["$ack", true] }, 1, 0], + }, + }, + downChecks: { + $sum: { + $cond: [ + { $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, + 1, + 0, + ], + }, + }, + cannotResolveChecks: { + $sum: { + $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0], + }, + }, + }, + }, + { + $project: { + _id: 0, + }, + }, + ], + }, + }, + { + $project: { + summary: { $arrayElemAt: ["$summary", 0] }, + }, + }, + ]; +}; + +export { buildChecksAndSummaryByTeamIdPipeline }; From 644de5f34a36ab43b442f5106179b0dca6984c85 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Tue, 1 Jul 2025 18:10:35 +0530 Subject: [PATCH 3/5] Feat: Hooks for Summary --- client/src/Hooks/checkHooks.js | 28 ++++++++++++++- .../Components/StatusBoxes/StatusBox.jsx | 22 ++++++------ .../Components/StatusBoxes/index.jsx | 35 ++++++++----------- client/src/Pages/Incidents/index.jsx | 8 +++-- client/src/Utils/NetworkService.js | 12 +++++++ server/controllers/checkController.js | 13 +++++++ server/db/mongo/modules/checkModule.js | 10 +++--- server/routes/checkRoute.js | 1 + 8 files changed, 91 insertions(+), 38 deletions(-) diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js index 98fb0cfd5..a7dae8b9a 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -122,4 +122,30 @@ const useFetchChecksByMonitor = ({ return [checks, checksCount, isLoading, networkError]; }; -export { useFetchChecksByMonitor, useFetchChecksTeam }; +const useFetchChecksSummaryByTeamId = () => { + const [summary, setSummary] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchSummary = async () => { + try { + setIsLoading(true); + + const res = await networkService.getChecksAndSummaryByTeamId(); + setSummary(res.data.data); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + + fetchSummary(); + }, []); + + return [summary, isLoading, networkError]; +}; + +export { useFetchChecksByMonitor, useFetchChecksTeam, useFetchChecksSummaryByTeamId }; diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx index ef7457a9e..ac3d1660e 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -22,31 +22,31 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } else { - color = theme.palette.accent.main; - icon = ( - - - - ); - } + color = theme.palette.accent.main; + icon = ( + + + + ); + } return ( { +const StatusBoxes = ({ isLoading, summary }) => { const theme = useTheme(); if (isLoading) return ; return ( @@ -15,35 +15,30 @@ const StatusBoxes = ({ isLoading, statusCounts }) => { > + + - - ); }; StatusBoxes.propTypes = { isLoading: PropTypes.bool, - statusCounts: PropTypes.shape({ - total: PropTypes.number, - resolved: PropTypes.number, - cannotResolve: PropTypes.number, - down: PropTypes.number, - }), + summary: PropTypes.object, }; -export default StatusBoxes; \ No newline at end of file +export default StatusBoxes; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 708b443a5..11946ae63 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -9,6 +9,7 @@ import StatusBoxes from "./Components/StatusBoxes"; //Utils import { useTheme } from "@emotion/react"; import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks"; +import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks"; import { useState, useEffect } from "react"; import NetworkError from "../../Components/GenericFallback/NetworkError"; import { useTranslation } from "react-i18next"; @@ -32,6 +33,8 @@ const Incidents = () => { //Utils const theme = useTheme(); const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); + const [summary, isLoadingSummary, networkErrorSummary] = + useFetchChecksSummaryByTeamId(); const { monitorId } = useParams(); useEffect(() => { @@ -52,7 +55,7 @@ const Incidents = () => { setMonitorLookup(monitorLookup); }, [monitors]); - if (networkError) { + if (networkError || networkErrorSummary) { return ( @@ -64,7 +67,8 @@ const Incidents = () => { } The response from the axios GET request. + */ + getChecksAndSummaryByTeamId = async () => { + return this.axiosInstance.get(`/checks/team/summary`); + }; + /** * ************************************ * Update the status of a check diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js index 8f9d0fbb5..e50d5b7fc 100755 --- a/server/controllers/checkController.js +++ b/server/controllers/checkController.js @@ -111,6 +111,19 @@ class CheckController { } }; + getChecksSummaryByTeamId = async (req, res, next) => { + try { + const { teamId } = req.user; + const summary = await this.db.getChecksSummaryByTeamId({ teamId }); + return res.success({ + msg: this.stringService.checkGetSummary, + data: summary, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId")); + } + }; + ackCheck = async (req, res, next) => { try { await ackCheckBodyValidation.validateAsync(req.body); diff --git a/server/db/mongo/modules/checkModule.js b/server/db/mongo/modules/checkModule.js index 835f358fa..7c9682557 100755 --- a/server/db/mongo/modules/checkModule.js +++ b/server/db/mongo/modules/checkModule.js @@ -312,12 +312,14 @@ const ackAllChecks = async (monitorId, teamId, ack, path) => { * @returns {Promise} * @throws {Error} */ -const getChecksAndSummaryByTeamId = async ({ teamId }) => { +const getChecksSummaryByTeamId = async ({ teamId }) => { try { const matchStage = { teamId: new ObjectId(teamId), - } - const checks = await Check.aggregate(buildChecksAndSummaryByTeamIdPipeline({ matchStage })); + }; + const checks = await Check.aggregate( + buildChecksAndSummaryByTeamIdPipeline({ matchStage }) + ); return checks[0].summary; } catch (error) { error.service = SERVICE_NAME; @@ -409,7 +411,7 @@ export { getChecksByTeam, ackCheck, ackAllChecks, - getChecksAndSummaryByTeamId, + getChecksSummaryByTeamId, deleteChecks, deleteChecksByTeamId, updateChecksTTL, diff --git a/server/routes/checkRoute.js b/server/routes/checkRoute.js index 7f91dbe31..4ac1e3730 100755 --- a/server/routes/checkRoute.js +++ b/server/routes/checkRoute.js @@ -14,6 +14,7 @@ class CheckRoutes { initRoutes() { this.router.get("/team", this.checkController.getChecksByTeam); + this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); this.router.delete( "/team", isAllowed(["admin", "superadmin"]), From 278964500b7e88f7d6ff5856a422a9349ac36016 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Tue, 1 Jul 2025 18:23:11 +0530 Subject: [PATCH 4/5] Minor Changes --- .../Incidents/Components/StatusBoxes/StatusBox.jsx | 2 +- .../Pages/Incidents/Components/StatusBoxes/index.jsx | 10 ++++++---- client/src/locales/en.json | 3 ++- server/db/mongo/modules/checkModule.js | 6 +++--- server/db/mongo/modules/checkModuleQueries.js | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx index ac3d1660e..bc32fa765 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -108,7 +108,7 @@ const StatusBox = ({ title, value, status }) => { }; StatusBox.propTypes = { - title: PropTypes.string, + title: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, status: PropTypes.string, }; diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx index 9d4193743..6ba2f1820 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx @@ -2,10 +2,12 @@ import PropTypes from "prop-types"; import { Stack } from "@mui/material"; import StatusBox from "./StatusBox"; import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; import SkeletonLayout from "./skeleton"; const StatusBoxes = ({ isLoading, summary }) => { const theme = useTheme(); + const { t } = useTranslation(); if (isLoading) return ; return ( { justifyContent="space-between" > diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8d26f75fd..d39be206e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -385,7 +385,8 @@ "incidentsOptionsHeader": "Incidents for:", "incidentsOptionsHeaderFilterAll": "All", "incidentsOptionsHeaderFilterBy": "Filter by:", - "incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve", + "incidentsOptionsHeaderTotalIncidents": "Total Incidents", + "incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve", "incidentsOptionsHeaderFilterDown": "Down", "incidentsOptionsHeaderFilterResolved": "Resolved", "incidentsOptionsHeaderLastDay": "Last day", diff --git a/server/db/mongo/modules/checkModule.js b/server/db/mongo/modules/checkModule.js index 7c9682557..fb0cc539e 100755 --- a/server/db/mongo/modules/checkModule.js +++ b/server/db/mongo/modules/checkModule.js @@ -5,7 +5,7 @@ import PageSpeedCheck from "../../models/PageSpeedCheck.js"; import User from "../../models/User.js"; import logger from "../../../utils/logger.js"; import { ObjectId } from "mongodb"; -import { buildChecksAndSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; +import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; const SERVICE_NAME = "checkModule"; const dateRangeLookup = { @@ -318,12 +318,12 @@ const getChecksSummaryByTeamId = async ({ teamId }) => { teamId: new ObjectId(teamId), }; const checks = await Check.aggregate( - buildChecksAndSummaryByTeamIdPipeline({ matchStage }) + buildChecksSummaryByTeamIdPipeline({ matchStage }) ); return checks[0].summary; } catch (error) { error.service = SERVICE_NAME; - error.method = "getChecksAndSummaryByTeamId"; + error.method = "getChecksSummaryByTeamId"; throw error; } }; diff --git a/server/db/mongo/modules/checkModuleQueries.js b/server/db/mongo/modules/checkModuleQueries.js index 097389a52..7795fb6dc 100644 --- a/server/db/mongo/modules/checkModuleQueries.js +++ b/server/db/mongo/modules/checkModuleQueries.js @@ -1,4 +1,4 @@ -const buildChecksAndSummaryByTeamIdPipeline = ({ matchStage }) => { +const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => { return [ { $match: matchStage }, { @@ -45,4 +45,4 @@ const buildChecksAndSummaryByTeamIdPipeline = ({ matchStage }) => { ]; }; -export { buildChecksAndSummaryByTeamIdPipeline }; +export { buildChecksSummaryByTeamIdPipeline }; From 3acfde97dfdb4211997688df8b415d1012e3beec Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Wed, 2 Jul 2025 12:51:27 +0530 Subject: [PATCH 5/5] Legacy UI Fixes --- .../Incidents/Components/StatusBoxes/StatusBox.jsx | 14 +++++++------- .../Monitors/Components/StatusBoxes/statusBox.jsx | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx index bc32fa765..b19f017e7 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -21,28 +21,28 @@ const StatusBox = ({ title, value, status }) => { if (status === "up") { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } else { color = theme.palette.accent.main; icon = ( - + ); @@ -84,15 +84,15 @@ const StatusBox = ({ title, value, status }) => { {value} { if (status === "up") { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); @@ -75,15 +75,15 @@ const StatusBox = ({ title, value, status }) => { {value}