From b9bcc893b0ae786c54410a4468e129c58f3961dd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 5 Feb 2025 13:31:41 -0800 Subject: [PATCH 01/14] Add DU Monitors page --- .../DistributedUptime/Monitors/index.jsx | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 Client/src/Pages/DistributedUptime/Monitors/index.jsx diff --git a/Client/src/Pages/DistributedUptime/Monitors/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/index.jsx new file mode 100644 index 000000000..e8fc9421d --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Monitors/index.jsx @@ -0,0 +1,187 @@ +// Components +import { Stack, Box, Button } from "@mui/material"; +import DataTable from "../../Components/Table"; +import Breadcrumbs from "../../Components/Breadcrumbs"; +import Host from "../Uptime/Home/host"; +import BarChart from "../../Components/Charts/BarChart"; +import ActionsMenu from "../Uptime/Home/actionsMenu"; +import { StatusLabel } from "../../Components/Label"; +// Utils +import { networkService } from "../../main"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import useUtils from "../Uptime/utils"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +// Constants +const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }]; +const TYPE_MAP = { + distributed_http: "Distributed HTTP", +}; + +const DistributedUptimeMonitors = () => { + // Redux state + const { authToken, user } = useSelector((state) => state.auth); + // Local state + const [monitors, setMonitors] = useState([]); + const [filteredMonitors, setFilteredMonitors] = useState([]); + const [monitorsSummary, setMonitorsSummary] = useState({}); + // Utils + + const { determineState } = useUtils(); + const theme = useTheme(); + const navigate = useNavigate(); + const headers = [ + { + id: "name", + content: Host, + render: (row) => ( + + ), + }, + { + id: "status", + content: Status, + render: (row) => { + const status = determineState(row?.monitor); + return ( + + ); + }, + }, + { + id: "responseTime", + content: "Response Time", + render: (row) => , + }, + { + id: "type", + content: "Type", + render: (row) => {TYPE_MAP[row.monitor.type]}, + }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + ), + }, + ]; + + const getMonitorWithPercentage = (monitor, theme) => { + let uptimePercentage = ""; + let percentageColor = theme.palette.percentage.uptimeExcellent; + + 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, + name: monitor.name, + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + monitor: monitor, + }; + }; + + useEffect(() => { + const cleanup = networkService.subscribeToDistributedUptimeMonitors({ + authToken: authToken, + teamId: user.teamId, + limit: 25, + types: ["distributed_http"], + page: 0, + rowsPerPage: 10, + filter: null, + field: null, + order: null, + onUpdate: (data) => { + const res = data.monitors; + const { monitors, filteredMonitors, summary } = res; + const mappedMonitors = filteredMonitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(monitors); + setFilteredMonitors(mappedMonitors); + setMonitorsSummary(summary); + }, + }); + + return cleanup; + }, [user.teamId, authToken, theme]); + + return ( + + + + + + {monitors.length > 0 && ( + { + navigate(`/distributed-uptime/${row.id}`); + }, + }} + /> + )} + + ); +}; + +export default DistributedUptimeMonitors; From 85a5b83ab80b3b9df63840efc8232a0dde7dfee9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 5 Feb 2025 13:59:28 -0800 Subject: [PATCH 02/14] fix DU operations --- .../distributedUptimeController.js | 1 + Server/db/mongo/MongoDB.js | 6 +++++ .../mongo/modules/distributedCheckModule.js | 15 ++++++++++++ Server/db/mongo/modules/monitorModule.js | 24 +++++++++++++++++++ Server/index.js | 6 ++++- 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Server/db/mongo/modules/distributedCheckModule.js diff --git a/Server/controllers/distributedUptimeController.js b/Server/controllers/distributedUptimeController.js index ee8f5398e..09c1497cf 100644 --- a/Server/controllers/distributedUptimeController.js +++ b/Server/controllers/distributedUptimeController.js @@ -138,6 +138,7 @@ class DistributedUptimeController { clearInterval(keepAlive); }); } catch (error) { + console.log(error); next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors")); } } diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index e62828a41..8f035c2dc 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -43,6 +43,11 @@ import * as hardwareCheckModule from "./modules/hardwareCheckModule.js"; import * as checkModule from "./modules/checkModule.js"; +//**************************************** +// Distributed Checks +//**************************************** +import * as distributedCheckModule from "./modules/distributedCheckModule.js"; + //**************************************** // Maintenance Window //**************************************** @@ -74,6 +79,7 @@ class MongoDB { Object.assign(this, pageSpeedCheckModule); Object.assign(this, hardwareCheckModule); Object.assign(this, checkModule); + Object.assign(this, distributedCheckModule); Object.assign(this, maintenanceWindowModule); Object.assign(this, notificationModule); Object.assign(this, settingsModule); diff --git a/Server/db/mongo/modules/distributedCheckModule.js b/Server/db/mongo/modules/distributedCheckModule.js new file mode 100644 index 000000000..5c7fb09a8 --- /dev/null +++ b/Server/db/mongo/modules/distributedCheckModule.js @@ -0,0 +1,15 @@ +import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js"; +const SERVICE_NAME = "distributedCheckModule"; + +const createDistributedCheck = async (checkData) => { + try { + const check = await new DistributedUptimeCheck({ ...checkData }).save(); + return check; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createCheck"; + throw error; + } +}; + +export { createDistributedCheck }; diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index c3d1a38be..66ff2df6e 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -674,6 +674,26 @@ const getMonitorsByTeamId = async (req) => { }, ] : []), + ...(limit + ? [ + { + $lookup: { + from: "distributeduptimechecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "distributeduptimechecks", + }, + }, + ] + : []), { $addFields: { @@ -692,6 +712,10 @@ const getMonitorsByTeamId = async (req) => { case: { $eq: ["$type", "hardware"] }, then: "$hardwarechecks", }, + { + case: { $eq: ["$type", "distributed_http"] }, + then: "$distributeduptimechecks", + }, ], default: [], }, diff --git a/Server/index.js b/Server/index.js index 7a4129bc9..23d16856a 100644 --- a/Server/index.js +++ b/Server/index.js @@ -244,7 +244,11 @@ const startApp = async () => { ServiceRegistry.get(MongoDB.SERVICE_NAME) ); - const distributedUptimeController = new DistributedUptimeController(); + const distributedUptimeController = new DistributedUptimeController( + ServiceRegistry.get(MongoDB.SERVICE_NAME), + http, + ServiceRegistry.get(StatusService.SERVICE_NAME) + ); //Create routes const authRoutes = new AuthRoutes(authController); From 68c772785bbe89504be510e18583e486f00c27d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 5 Feb 2025 13:59:41 -0800 Subject: [PATCH 03/14] Add DU network operations --- Client/src/Utils/NetworkService.js | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 85e82554e..20b19e5dc 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -862,6 +862,76 @@ class NetworkService { ); } + subscribeToDistributedUptimeMonitors(config) { + const { + authToken, + teamId, + onUpdate, + onError, + onOpen, + limit, + types, + page, + rowsPerPage, + filter, + field, + order, + } = config; + + const params = new URLSearchParams(); + + if (limit) params.append("limit", limit); + if (types) { + types.forEach((type) => { + params.append("type", type); + }); + } + if (page) params.append("page", page); + if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); + if (order) params.append("order", order); + + if (this.eventSource) { + this.eventSource.close(); + } + + const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}?${params.toString()}`; + this.eventSource = new EventSource(url, { + headers: { Authorization: `Bearer ${authToken}` }, + }); + + this.eventSource.onopen = () => { + onOpen?.(); + }; + + this.eventSource.addEventListener("open", (e) => { + console.log("getDistributedUptimeMonitors connection opened:"); + }); + + this.eventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + onUpdate(data); + }; + + this.eventSource.onerror = (error) => { + console.error("Monitor stream error:", error); + onError?.(); + this.eventSource.close(); + }; + + // Returns a cleanup function + return () => { + if (this.eventSource) { + this.eventSource.close(); + this.eventSource = null; + } + return () => { + console.log("Nothing to cleanup"); + }; + }; + } + async getStatusPage(config) { const { authToken } = config; return this.axiosInstance.get(`/status-page`, { From b43954607106bbd6958916b64e1ce06f5f60a41a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 5 Feb 2025 13:59:56 -0800 Subject: [PATCH 04/14] Add DU Monitors page --- .../DistributedUptime/Monitors/index.jsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Client/src/Pages/DistributedUptime/Monitors/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/index.jsx index e8fc9421d..0d1288aa9 100644 --- a/Client/src/Pages/DistributedUptime/Monitors/index.jsx +++ b/Client/src/Pages/DistributedUptime/Monitors/index.jsx @@ -1,16 +1,16 @@ // Components import { Stack, Box, Button } from "@mui/material"; -import DataTable from "../../Components/Table"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import Host from "../Uptime/Home/host"; -import BarChart from "../../Components/Charts/BarChart"; -import ActionsMenu from "../Uptime/Home/actionsMenu"; -import { StatusLabel } from "../../Components/Label"; +import DataTable from "../../../Components/Table"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Host from "../../Uptime/Monitors/Components/Host"; +import BarChart from "../../../Components/Charts/BarChart"; +import ActionsMenu from "../../Uptime/Monitors/Components/ActionsMenu"; +import { StatusLabel } from "../../../Components/Label"; // Utils -import { networkService } from "../../main"; +import { networkService } from "../../../main"; import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; -import useUtils from "../Uptime/utils"; +import useUtils from "../../Uptime/Monitors/Hooks/useUtils"; import { useEffect, useState } from "react"; import { useSelector } from "react-redux"; @@ -84,7 +84,7 @@ const DistributedUptimeMonitors = () => { const getMonitorWithPercentage = (monitor, theme) => { let uptimePercentage = ""; - let percentageColor = theme.palette.percentage.uptimeExcellent; + let percentageColor = ""; if (monitor.uptimePercentage !== undefined) { uptimePercentage = @@ -94,12 +94,12 @@ const DistributedUptimeMonitors = () => { percentageColor = monitor.uptimePercentage < 0.25 - ? theme.palette.percentage.uptimePoor + ? theme.palette.error.main : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair + ? theme.palette.warning.main : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.percentage.uptimeExcellent; + ? theme.palette.success.main + : theme.palette.success.main; } return { From 823358125b5afd4aae87a5b3fb64289af290ef89 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 5 Feb 2025 14:00:11 -0800 Subject: [PATCH 05/14] Add create DU Monitor page --- .../Pages/DistributedUptime/Create/index.jsx | 307 ++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 Client/src/Pages/DistributedUptime/Create/index.jsx diff --git a/Client/src/Pages/DistributedUptime/Create/index.jsx b/Client/src/Pages/DistributedUptime/Create/index.jsx new file mode 100644 index 000000000..2df0a4482 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Create/index.jsx @@ -0,0 +1,307 @@ +// Components +import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; + +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import ConfigBox from "../../../Components/ConfigBox"; +import TextInput from "../../../Components/Inputs/TextInput"; +import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; +import Radio from "../../../Components/Inputs/Radio"; +import Checkbox from "../../../Components/Inputs/Checkbox"; +import Select from "../../../Components/Inputs/Select"; +import { createToast } from "../../../Utils/toastUtils"; + +// Utility +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useSelector, useDispatch } from "react-redux"; +import { monitorValidation } from "../../../Validation/validation"; +import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; + +// Constants +const BREADCRUMBS = [ + { name: `distributed uptime`, path: "/distributed-uptime" }, + { name: "create", path: `/distributed-uptime/create` }, +]; +const MS_PER_MINUTE = 60000; +const SELECT_VALUES = [ + { _id: 1, name: "1 minute" }, + { _id: 2, name: "2 minutes" }, + { _id: 3, name: "3 minutes" }, + { _id: 4, name: "4 minutes" }, + { _id: 5, name: "5 minutes" }, +]; + +const CreateDistributedUptime = () => { + // Redux state + const { user, authToken } = useSelector((state) => state.auth); + const isLoading = useSelector((state) => state.uptimeMonitors.isLoading); + + // Local state + const [https, setHttps] = useState(true); + const [notifications, setNotifications] = useState([]); + const [monitor, setMonitor] = useState({ + type: "distributed_http", + name: "", + url: "", + interval: 1, + }); + const [errors, setErrors] = useState({}); + + //utils + const theme = useTheme(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + // Handlers + const handleCreateMonitor = async (event) => { + const monitorToSubmit = { ...monitor }; + + // Prepend protocol to url + monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url; + + const { error } = monitorValidation.validate(monitorToSubmit, { + abortEarly: false, + }); + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + createToast({ body: "Please check the form for errors." }); + return; + } + + // Append needed fields + monitorToSubmit.description = monitor.name; + monitorToSubmit.interval = monitor.interval * MS_PER_MINUTE; + monitorToSubmit.teamId = user.teamId; + monitorToSubmit.userId = user._id; + monitorToSubmit.notifications = notifications; + + const action = await dispatch( + createUptimeMonitor({ authToken, monitor: monitorToSubmit }) + ); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor created successfully!" }); + navigate("/distributed-uptime"); + } else { + createToast({ body: "Failed to create monitor." }); + } + }; + + const handleChange = (event) => { + const { name, value } = event.target; + setMonitor({ + ...monitor, + [name]: value, + }); + const { error } = monitorValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + console.log(name); + setErrors((prev) => ({ + ...prev, + ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), + })); + }; + + const handleNotifications = (event, type) => { + const { value } = event.target; + let currentNotifications = [...notifications]; + const notificationAlreadyExists = notifications.some((notification) => { + if (notification.type === type && notification.address === value) { + return true; + } + return false; + }); + if (notificationAlreadyExists) { + currentNotifications = currentNotifications.filter((notification) => { + if (notification.type === type && notification.address === value) { + return false; + } + return true; + }); + } else { + currentNotifications.push({ type, address: value }); + } + setNotifications(currentNotifications); + }; + + return ( + + + console.log("submit")} + > + + + Create your{" "} + + + monitor + + + + + General settings + + Here you can select the URL of the host, together with the type of monitor. + + + + } + label="URL to monitor" + https={https} + placeholder={"www.google.com"} + value={monitor.url} + name="url" + onChange={handleChange} + error={errors["url"] ? true : false} + helperText={errors["url"]} + /> + + + + + + Checks to perform + + You can always add or remove checks after adding your site. + + + + + + {monitor.type === "http" || monitor.type === "distributed_http" ? ( + + + + + ) : ( + "" + )} + + + {errors["type"] ? ( + + + {errors["type"]} + + + ) : ( + "" + )} + + + + + Incident notifications + + When there is an incident, notify users. + + + + notification.type === "email" + )} + value={user?.email} + onChange={(event) => handleNotifications(event, "email")} + /> + + + + + Advanced settings + + +