From 899111e48bfa9c245da41968353cef5414862f16 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 10:23:26 -0700 Subject: [PATCH 01/14] Add loading button to createPageSpeed --- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 00c02a362..4c8204153 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -1,4 +1,6 @@ -import { Box, Button, Stack, Typography } from "@mui/material"; +import { Box, Stack, Typography } from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; + import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; @@ -12,11 +14,11 @@ import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMon import Breadcrumbs from "../../../Components/Breadcrumbs"; import "./index.css"; import { logger } from "../../../Utils/Logger"; - const CreatePageSpeed = () => { const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); + const { isLoading } = useSelector((state) => state.pageSpeedMonitors); const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); @@ -226,7 +228,8 @@ const CreatePageSpeed = () => { - + From e00938e12a832669a3d25757065eca7fb2bb056d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 10:34:24 -0700 Subject: [PATCH 02/14] Add loading state to team panel --- .../TabPanels/Account/TeamPanel.jsx | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index 8c4630b19..9809ac710 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -1,25 +1,13 @@ import { useTheme } from "@emotion/react"; import TabPanel from "@mui/lab/TabPanel"; -import { - Box, - Button, - ButtonGroup, - Divider, - IconButton, - Modal, - Stack, - TextField, - Typography, -} from "@mui/material"; +import { Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; -import EditSvg from "../../../assets/icons/edit.svg?react"; import Field from "../../Inputs/Field"; import { credentials } from "../../../Validation/validation"; import { networkService } from "../../../main"; import { createToast } from "../../../Utils/toastUtils"; import { useSelector } from "react-redux"; import BasicTable from "../../BasicTable"; -import Remove from "../../../assets/icons/trash-bin.svg?react"; import Select from "../../Inputs/Select"; import LoadingButton from "@mui/lab/LoadingButton"; @@ -54,6 +42,7 @@ const TeamPanel = () => { const [members, setMembers] = useState([]); const [filter, setFilter] = useState("all"); const [errors, setErrors] = useState({}); + const [isSendingInvite, setIsSendingInvite] = useState(false); useEffect(() => { const fetchTeam = async () => { @@ -173,6 +162,7 @@ const TeamPanel = () => { }; const handleInviteMember = async () => { + setIsSendingInvite(true); if (!toInvite.role.includes("user") || !toInvite.role.includes("admin")) setToInvite((prev) => ({ ...prev, role: ["user"] })); @@ -185,24 +175,28 @@ const TeamPanel = () => { if (error) { setErrors((prev) => ({ ...prev, email: error.details[0].message })); - } else - try { - await networkService.requestInvitationToken( - authToken, - toInvite.email, - toInvite.role - ); + return; + } - closeInviteModal(); - createToast({ - body: "Member invited. They will receive an email with details on how to create their account.", - }); - } catch (error) { - createToast({ - body: error.message || "Unknown error.", - }); - } + try { + await networkService.requestInvitationToken( + authToken, + toInvite.email, + toInvite.role + ); + closeInviteModal(); + createToast({ + body: "Member invited. They will receive an email with details on how to create their account.", + }); + } catch (error) { + createToast({ + body: error.message || "Unknown error.", + }); + } finally { + setIsSendingInvite(false); + } }; + const closeInviteModal = () => { setIsOpen(false); setToInvite({ email: "", role: ["0"] }); @@ -307,13 +301,14 @@ const TeamPanel = () => { - + { mt={theme.spacing(8)} justifyContent="flex-end" > - + Send invite From 560ad118e7612a43ad906bd6f5a2a17cfe4aeb5e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 10:37:26 -0700 Subject: [PATCH 03/14] Add loading button to settings page --- Client/src/Pages/Settings/index.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index fc615d848..d1f15ea8c 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -16,6 +16,9 @@ const Settings = ({ isAdmin }) => { const { isLoading } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); + + // TODO Handle saving + const handleClearStats = async () => { try { const action = await dispatch( @@ -156,13 +159,14 @@ const Settings = ({ isAdmin }) => { - + From 0d8f3f3666464eec17965787bb2b203a924100d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 10:38:45 -0700 Subject: [PATCH 04/14] Add loading button to config page --- Client/src/Pages/Monitors/Configure/index.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index 483a2fd79..6e3caeff7 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -275,14 +275,15 @@ const Configure = () => { )} - + From 789e6e91c472a93c9feda5b0961c27b26b9df995 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 12:07:59 -0700 Subject: [PATCH 05/14] add thunks for getting pagespeed monitor --- .../PageSpeedMonitor/pageSpeedMonitorSlice.js | 78 +++++++++- .../UptimeMonitors/uptimeMonitorsSlice.js | 2 +- .../src/Pages/PageSpeed/Configure/index.jsx | 133 +++++++++--------- .../Pages/PageSpeed/Configure/skeleton.jsx | 51 +++++++ 4 files changed, 191 insertions(+), 73 deletions(-) create mode 100644 Client/src/Pages/PageSpeed/Configure/skeleton.jsx diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index e32d673cc..317ab1320 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -28,6 +28,26 @@ export const createPageSpeed = createAsyncThunk( } ); +export const getPagespeedMonitorById = createAsyncThunk( + "monitors/getMonitorById", + async (data, thunkApi) => { + try { + const { authToken, monitorId } = data; + const res = await networkService.getMonitorByid(authToken, monitorId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + export const getPageSpeedByTeamId = createAsyncThunk( "pageSpeedMonitors/getPageSpeedByTeamId", async (token, thunkApi) => { @@ -109,6 +129,25 @@ export const deletePageSpeed = createAsyncThunk( } } ); +export const pausePageSpeed = createAsyncThunk( + "pageSpeedMonitors/pausePageSpeed", + async (data, thunkApi) => { + try { + const { authToken, monitorId } = data; + const res = await networkService.pauseMonitorById(authToken, monitorId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); const pageSpeedMonitorSlice = createSlice({ name: "pageSpeedMonitor", @@ -124,7 +163,7 @@ const pageSpeedMonitorSlice = createSlice({ extraReducers: (builder) => { builder // ***************************************************** - // Monitors by userId + // Monitors by teamId // ***************************************************** .addCase(getPageSpeedByTeamId.pending, (state) => { @@ -143,6 +182,23 @@ const pageSpeedMonitorSlice = createSlice({ : "Getting page speed monitors failed"; }) + // ***************************************************** + .addCase(getPagespeedMonitorById.pending, (state) => { + state.isLoading = true; + }) + .addCase(getPagespeedMonitorById.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(getPagespeedMonitorById.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to get pagespeed monitor"; + }) + // ***************************************************** // Create Monitor // ***************************************************** @@ -163,7 +219,7 @@ const pageSpeedMonitorSlice = createSlice({ }) // ***************************************************** - // Create Monitor + // Update Monitor // ***************************************************** .addCase(updatePageSpeed.pending, (state) => { state.isLoading = true; @@ -198,6 +254,24 @@ const pageSpeedMonitorSlice = createSlice({ state.msg = action.payload ? action.payload.msg : "Failed to delete page speed monitor"; + }) + // ***************************************************** + // Pause Monitor + // ***************************************************** + .addCase(pausePageSpeed.pending, (state) => { + state.isLoading = true; + }) + .addCase(pausePageSpeed.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(pausePageSpeed.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to pause page speed monitor"; }); }, }); diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 2c381812c..895bd1493 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -236,7 +236,7 @@ const uptimeMonitorsSlice = createSlice({ state.success = false; state.msg = action.payload ? action.payload.msg - : "Failed to pause uptime monitor"; + : "Failed to get uptime monitor"; }) // ***************************************************** // update Monitor diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index 92dd5bb36..005583f2c 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -1,12 +1,14 @@ import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; -import { Box, Button, Modal, Skeleton, Stack, Typography } from "@mui/material"; +import { Box, Button, Modal, Stack, Typography } from "@mui/material"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router"; import { deletePageSpeed, + getPagespeedMonitorById, getPageSpeedByTeamId, updatePageSpeed, + pausePageSpeed, } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; @@ -17,63 +19,18 @@ import Checkbox from "../../../Components/Inputs/Checkbox"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import PulseDot from "../../../Components/Animated/PulseDot"; - +import LoadingButton from "@mui/lab/LoadingButton"; +import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; +import SkeletonLayout from "./skeleton"; import "./index.css"; -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - ); -}; - const PageSpeedConfigure = () => { const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); const MS_PER_MINUTE = 60000; const { authToken } = useSelector((state) => state.auth); - const { monitors } = useSelector((state) => state.pageSpeedMonitors); + const { isLoading } = useSelector((state) => state.pageSpeedMonitors); const { monitorId } = useParams(); const [monitor, setMonitor] = useState({}); const [errors, setErrors] = useState({}); @@ -89,15 +46,25 @@ const PageSpeedConfigure = () => { ]; useEffect(() => { - const data = monitors.find((monitor) => monitor._id === monitorId); - if (!data) { - logger.error("Error fetching pagespeed monitor of id: " + monitorId); - navigate("/not-found", { replace: true }); - } - setMonitor({ - ...data, - }); - }, [monitorId, monitors, navigate]); + const fetchMonitor = async () => { + try { + const action = await dispatch( + getPagespeedMonitorById({ authToken, monitorId }) + ); + + if (getPagespeedMonitorById.fulfilled.match(action)) { + const monitor = action.payload.data; + setMonitor(monitor); + } else if (getPagespeedMonitorById.rejected.match(action)) { + throw new Error(action.error.message); + } + } catch (error) { + logger.error("Error fetching monitor of id: " + monitorId); + navigate("/not-found", { replace: true }); + } + }; + fetchMonitor(); + }, [dispatch, authToken, monitorId, navigate]); const handleChange = (event, id) => { let { value } = event.target; @@ -119,6 +86,21 @@ const PageSpeedConfigure = () => { }); }; + const handlePause = async () => { + try { + const action = await dispatch(pausePageSpeed({ authToken, monitorId })); + if (pausePageSpeed.fulfilled.match(action)) { + const monitor = action.payload.data; + setMonitor(monitor); + } else if (pausePageSpeed.rejected.match(action)) { + throw new Error(action.error.message); + } + } catch (error) { + logger.error("Error pausing monitor: " + monitorId); + createToast({ body: "Failed to pause monitor" }); + } + }; + const handleSave = async (event) => { event.preventDefault(); const action = await dispatch( @@ -143,11 +125,9 @@ const PageSpeedConfigure = () => { } }; - let loading = Object.keys(monitor).length === 0; - return ( - {loading ? ( + {Object.keys(monitor).length === 0 ? ( ) : ( <> @@ -195,7 +175,9 @@ const PageSpeedConfigure = () => { - - + { - + diff --git a/Client/src/Pages/PageSpeed/Configure/skeleton.jsx b/Client/src/Pages/PageSpeed/Configure/skeleton.jsx new file mode 100644 index 000000000..301a00e52 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Configure/skeleton.jsx @@ -0,0 +1,51 @@ +import { Box, Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SkeletonLayout; From bc0df325d1043fc4b2fa0caa414724045a5253e7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 14:39:16 -0700 Subject: [PATCH 06/14] Add route, controller, db operations, and validation for aggregate monitor stats --- Client/src/Utils/NetworkService.js | 2 +- Server/controllers/monitorController.js | 42 +++++++++++- Server/db/mongo/MongoDB.js | 2 + Server/db/mongo/modules/monitorModule.js | 83 ++++++++++++++++++++++++ Server/routes/monitorRoute.js | 1 + Server/validation/joi.js | 9 +++ 6 files changed, 137 insertions(+), 2 deletions(-) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 31a23b5b1..6dcde82bb 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -116,7 +116,7 @@ class NetworkService { * @param {string} [sortOrder] - The order in which to sort the retrieved statistics. * @param {number} [limit] - The maximum number of statistics to retrieve. * @param {string} [dateRange] - The date range for which to retrieve statistics. - * @param {number} [numToDisplay] - The number of statistics to display. + * @param {number} [numToDisplay] - The number of checks to display. * @param {boolean} [normalize] - Whether to normalize the retrieved statistics. * @returns {Promise} The response from the axios GET request. */ diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index f33ddcf10..0d831f5b6 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -6,12 +6,13 @@ const { editMonitorBodyValidation, getMonitorsByTeamIdQueryValidation, pauseMonitorParamValidation, + getMonitorAggregateStatsParamValidation, + getMonitorAggregateStatsQueryValidation, } = require("../validation/joi"); const sslChecker = require("ssl-checker"); const SERVICE_NAME = "monitorController"; const { errorMessages, successMessages } = require("../utils/messages"); -const { runInNewContext } = require("vm"); /** * Returns all monitors @@ -35,6 +36,44 @@ const getAllMonitors = async (req, res, next) => { } }; +/** + * Returns agregate stats for a monitor + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ + +const getMonitorAggregateStats = async (req, res, next) => { + try { + await getMonitorAggregateStatsParamValidation.validateAsync(req.params); + await getMonitorAggregateStatsQueryValidation.validateAsync(req.query); + } catch (error) { + error.status = 422; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + try { + const { monitorId } = req.params; + const dateRange = req.query.dateRange; + const aggregateStats = await req.db.getMonitorAggregateStats( + monitorId, + dateRange + ); + return res.json({ + success: true, + msg: successMessages.MONTIOR_STATS_BY_ID, + data: aggregateStats, + }); + } catch (error) { + next(error); + } +}; + /** * Returns monitor stats for monitor with matching ID * @async @@ -376,6 +415,7 @@ const pauseMonitor = async (req, res, next) => { module.exports = { getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorCertificate, getMonitorById, diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 912bfb438..6f959f758 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -64,6 +64,7 @@ const { const { getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, @@ -144,6 +145,7 @@ module.exports = { resetPassword, checkSuperadmin, getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 79e8917ea..0bd054000 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -143,6 +143,88 @@ const getStatusBarValues = (monitor, checks) => { } return statusBarValues.reverse(); }; + +/** + * Get aggregate monitor stats for charts + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ +const getMonitorAggregateStats = async (monitorId, dateRange) => { + const startDates = { + day: new Date(new Date().setDate(new Date().getDate() - 1)), + + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + }; + + try { + const startDate = startDates[dateRange]; + if (!startDate) { + throw new Error("Invalid date range specified"); + } + const endDate = new Date(); + + const checks = await Check.find({ + monitorId: monitorId, + createdAt: { + $gte: startDate, + $lte: endDate, + }, + }); + + let groupedChecks; + // Group checks by hour if range is day + if (dateRange === "day") { + groupedChecks = checks.reduce((acc, check) => { + const hour = new Date(check.createdAt).getHours(); + if (!acc[hour]) { + acc[hour] = { hour, checks: [] }; + } + acc[hour].checks.push(check); + return acc; + }, {}); + } + + // Group checks by day if range is week or month + else { + groupedChecks = checks.reduce((acc, check) => { + const day = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part + if (!acc[day]) { + acc[day] = { day, checks: [] }; + } + acc[day].checks.push(check); + return acc; + }, {}); + } + + // Map grouped checks to stats + stats = Object.values(groupedChecks).map((group) => { + const totalChecks = group.checks.length; + const totalIncidents = group.checks.filter( + (check) => check.status === false + ).length; + const avgResponseTime = + group.checks.reduce((sum, check) => sum + check.responseTime, 0) / + totalChecks; + + return { + hour: group.hour, + day: group.day, + totalChecks, + totalIncidents, + avgResponseTime: `${avgResponseTime.toFixed(2)}ms`, + }; + }); + + return stats; + } catch (error) { + throw error; + } +}; + /** * Get stats by monitor ID * @async @@ -452,6 +534,7 @@ const editMonitor = async (candidateId, candidateMonitor) => { module.exports = { getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index ee1c08488..7831f42f7 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -3,6 +3,7 @@ const monitorController = require("../controllers/monitorController"); const { isAllowed } = require("../middleware/isAllowed"); router.get("/", monitorController.getAllMonitors); +router.get("/aggregate/:monitorId", monitorController.getMonitorAggregateStats); router.get("/stats/:monitorId", monitorController.getMonitorStatsById); router.get("/certificate/:monitorId", monitorController.getMonitorCertificate); router.get("/:monitorId", monitorController.getMonitorById); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index ccc8da9f6..629deddf5 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -201,6 +201,13 @@ const pauseMonitorParamValidation = joi.object({ monitorId: joi.string().required(), }); +const getMonitorAggregateStatsParamValidation = joi.object({ + monitorId: joi.string().required(), +}); +const getMonitorAggregateStatsQueryValidation = joi.object({ + dateRange: joi.string().valid("day", "week", "month"), +}); + //**************************************** // Alerts //**************************************** @@ -357,6 +364,8 @@ module.exports = { getMonitorByIdQueryValidation, getMonitorsByTeamIdValidation, getMonitorsByTeamIdQueryValidation, + getMonitorAggregateStatsParamValidation, + getMonitorAggregateStatsQueryValidation, editMonitorBodyValidation, pauseMonitorParamValidation, editUserParamValidation, From 99b4da347f0bb51ed5781699ca3f6c64336fd38d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 14:47:37 -0700 Subject: [PATCH 07/14] remove space --- Server/db/mongo/modules/monitorModule.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 0bd054000..11b4b49a3 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -155,7 +155,6 @@ const getStatusBarValues = (monitor, checks) => { const getMonitorAggregateStats = async (monitorId, dateRange) => { const startDates = { day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), month: new Date(new Date().setMonth(new Date().getMonth() - 1)), }; From 64fe1a3af58579913f08126a1015d0a326a17248 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 14:52:00 -0700 Subject: [PATCH 08/14] Return raw value instead of formatted string --- Server/db/mongo/modules/monitorModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 11b4b49a3..e0718a0bb 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -214,7 +214,7 @@ const getMonitorAggregateStats = async (monitorId, dateRange) => { day: group.day, totalChecks, totalIncidents, - avgResponseTime: `${avgResponseTime.toFixed(2)}ms`, + avgResponseTime, }; }); From 34ce3af412d483b04ddfedd367984fa5ec4142fe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 15:04:29 -0700 Subject: [PATCH 09/14] Add 7 days uptime percentage stat --- Server/db/mongo/modules/monitorModule.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index e0718a0bb..b913519b6 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -281,6 +281,10 @@ const getMonitorStatsById = async (req) => { ...checksQuery, createdAt: { $gte: filterLookup.day }, }; + const checksQuery7Days = { + ...checksQuery, + createdAt: { $gte: filterLookup.week }, + }; const checksQuery30Days = { ...checksQuery, createdAt: { $gte: filterLookup.month }, @@ -290,15 +294,18 @@ const getMonitorStatsById = async (req) => { createdAt: { $gte: filterLookup.hour }, }; - const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([ - model.find(checksQuery24Hours).sort({ createdAt: sortOrder }), - model.find(checksQuery30Days).sort({ createdAt: sortOrder }), - model.find(checksQuery60Mins).sort({ createdAt: sortOrder }), - ]); + const [checks24Hours, checks7Days, checks30Days, checks60Mins] = + await Promise.all([ + model.find(checksQuery24Hours).sort({ createdAt: sortOrder }), + model.find(checksQuery7Days).sort({ createdAt: sortOrder }), + model.find(checksQuery30Days).sort({ createdAt: sortOrder }), + model.find(checksQuery60Mins).sort({ createdAt: sortOrder }), + ]); // HTTP/PING Specific stats monitorStats.avgResponseTime24hours = getAverageResponseTime24Hours(checks24Hours); + monitorStats.uptime7Days = getUptimePercentage(checks7Days); monitorStats.uptime24Hours = getUptimePercentage(checks24Hours); monitorStats.uptime30Days = getUptimePercentage(checks30Days); monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins); From 5af81c729112d50fce4e8df48022fdc187353fa0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 15:43:12 -0700 Subject: [PATCH 10/14] restore controller --- Server/controllers/monitorController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index f33ddcf10..702b28be4 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -359,6 +359,7 @@ const pauseMonitor = async (req, res, next) => { await req.jobQueue.addJob(monitor._id, monitor); } monitor.isActive = !monitor.isActive; + monitor.status = undefined; monitor.save(); return res.status(200).json({ success: true, From c490722eae352a2807c04381d4eda2bee0e33273 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 15:45:11 -0700 Subject: [PATCH 11/14] restore netowrk service --- Server/service/networkService.js | 82 +++++++++++++++++++------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/Server/service/networkService.js b/Server/service/networkService.js index f5c333a60..863349dbf 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -15,44 +15,60 @@ class NetworkService { this.NETWORK_ERROR = 5000; } - async handleNotification(job, isAlive) { + async handleNotification(monitor, isAlive) { try { - const { _id } = job.data; - const monitor = await this.db.getMonitorById(_id); - if (monitor === null || monitor === undefined) { - logger.error(`Null Monitor: ${_id}`, { - method: "handleNotification", - service: this.SERVICE_NAME, - jobId: job.id, - }); - return; - } + let template = + isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; + let status = isAlive === true ? "up" : "down"; - // If monitor status changes, update monitor status and send notification - if (monitor.status !== isAlive) { - monitor.status = !monitor.status; - await monitor.save(); - - let template = - isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; - let status = isAlive === true ? "up" : "down"; - - const notifications = await this.db.getNotificationsByMonitorId(_id); - for (const notification of notifications) { - if (notification.type === "email") { - await this.emailService.buildAndSendEmail( - template, - { monitorName: monitor.name, monitorUrl: monitor.url }, - notification.address, - `Monitor ${monitor.name} is ${status}` - ); - } + const notifications = await this.db.getNotificationsByMonitorId( + monitor._id + ); + for (const notification of notifications) { + if (notification.type === "email") { + await this.emailService.buildAndSendEmail( + template, + { monitorName: monitor.name, monitorUrl: monitor.url }, + notification.address, + `Monitor ${monitor.name} is ${status}` + ); } } } catch (error) { logger.error(error.message, { method: "handleNotification", service: this.SERVICE_NAME, + monitorId: monitor._id, + }); + } + } + + async handleStatusUpdate(job, isAlive) { + try { + const { _id } = job.data; + const monitor = await this.db.getMonitorById(_id); + + if (monitor === null || monitor === undefined) { + logger.error(`Null Monitor: ${_id}`, { + method: "handleStatusUpdate", + service: this.SERVICE_NAME, + jobId: job.id, + }); + return; + } + if (monitor.status === undefined || monitor.status !== isAlive) { + const oldStatus = monitor.status; + monitor.status = isAlive; + await monitor.save(); + + if (oldStatus !== undefined && oldStatus !== isAlive) { + this.handleNotification(monitor, isAlive); + } + } + } catch (error) { + logger.error(error.message, { + method: "handleStatusUpdate", + service: this.SERVICE_NAME, jobId: job.id, }); } @@ -113,7 +129,7 @@ class NetworkService { }; return await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { - this.handleNotification(job, isAlive); + this.handleStatusUpdate(job, isAlive); } } @@ -161,7 +177,7 @@ class NetworkService { return await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { - this.handleNotification(job, isAlive); + this.handleStatusUpdate(job, isAlive); } } @@ -241,7 +257,7 @@ class NetworkService { }; this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); } finally { - this.handleNotification(job, isAlive); + this.handleStatusUpdate(job, isAlive); } } From f52285b32ff13ac099ca2686ab393d321bb68b89 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 15:46:17 -0700 Subject: [PATCH 12/14] Restore job queue --- Server/service/jobQueue.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index ed15e0f94..5248c88fa 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -238,7 +238,6 @@ class JobQueue { console.log("Adding job", payload.url); // Execute job immediately await this.queue.add(jobName, payload); - await this.queue.add(jobName, payload, { repeat: { every: payload.interval, From 587cd1777ab3a77d6373331afe0ff01d69fccb66 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 15:47:20 -0700 Subject: [PATCH 13/14] restore label --- Client/src/Components/Label/index.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/Label/index.jsx b/Client/src/Components/Label/index.jsx index 0799a87d7..96468ffd6 100644 --- a/Client/src/Components/Label/index.jsx +++ b/Client/src/Components/Label/index.jsx @@ -126,6 +126,7 @@ ColoredLabel.propTypes = { */ const StatusLabel = ({ status, text, customStyles }) => { + console.log(status); const theme = useTheme(); const colors = { up: { @@ -138,6 +139,11 @@ const StatusLabel = ({ status, text, customStyles }) => { bgColor: theme.palette.error.bg, borderColor: theme.palette.error.light, }, + unknown: { + dotColor: theme.palette.unresolved.main, + bgColor: theme.palette.unresolved.bg, + borderColor: theme.palette.unresolved.light, + }, "cannot resolve": { dotColor: theme.palette.unresolved.main, bgColor: theme.palette.unresolved.bg, @@ -170,7 +176,7 @@ const StatusLabel = ({ status, text, customStyles }) => { }; StatusLabel.propTypes = { - status: PropTypes.oneOf(["up", "down", "cannot resolve"]), + status: PropTypes.oneOf(["up", "down", "unknown", "cannot resolve"]), text: PropTypes.string, customStyles: PropTypes.object, }; From 9110e0b6cce672bff433adebc38d76aee21fa003 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Sep 2024 16:01:24 -0700 Subject: [PATCH 14/14] restore monitor --- Server/models/Monitor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index fdc147d1b..d91275e23 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -24,7 +24,7 @@ const MonitorSchema = mongoose.Schema( }, status: { type: Boolean, - default: true, + default: undefined, }, type: { type: String, @@ -46,7 +46,7 @@ const MonitorSchema = mongoose.Schema( }, uptimePercentage: { type: Number, - default: undefined, + default: undefined, }, notifications: [ {