From 0583bcb0b194bf9229b3d3a62578dc3c60ddcf6e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 2 Jan 2025 10:54:30 -0800 Subject: [PATCH 01/64] Add healthcheck to redis image --- Docker/dev/docker-compose.yaml | 6 ++++++ Docker/dist/docker-compose.yaml | 6 ++++++ Docker/prod/docker-compose.yaml | 6 ++++++ Docker/test/docker-compose.yaml | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/Docker/dev/docker-compose.yaml b/Docker/dev/docker-compose.yaml index d3b00adf6..f8b2c6936 100644 --- a/Docker/dev/docker-compose.yaml +++ b/Docker/dev/docker-compose.yaml @@ -24,6 +24,12 @@ services: - "6379:6379" volumes: - ./redis/data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s mongodb: image: uptime_database_mongo:latest restart: always diff --git a/Docker/dist/docker-compose.yaml b/Docker/dist/docker-compose.yaml index f12c0979d..a73b75dc6 100644 --- a/Docker/dist/docker-compose.yaml +++ b/Docker/dist/docker-compose.yaml @@ -29,6 +29,12 @@ services: - "6379:6379" volumes: - ./redis/data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s mongodb: image: bluewaveuptime/uptime_database_mongo:latest restart: always diff --git a/Docker/prod/docker-compose.yaml b/Docker/prod/docker-compose.yaml index 67bfe42e8..2f8907089 100644 --- a/Docker/prod/docker-compose.yaml +++ b/Docker/prod/docker-compose.yaml @@ -37,6 +37,12 @@ services: - "6379:6379" volumes: - ./redis/data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s mongodb: image: uptime_database_mongo:latest restart: always diff --git a/Docker/test/docker-compose.yaml b/Docker/test/docker-compose.yaml index 9f51edab8..40890e1d9 100644 --- a/Docker/test/docker-compose.yaml +++ b/Docker/test/docker-compose.yaml @@ -37,6 +37,12 @@ services: - "6379:6379" volumes: - ./redis/data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s mongodb: image: uptime_database_mongo:latest command: ["mongod", "--quiet", "--auth"] From ef13af3884d0de25907da82a8ac1af2d967e95ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 2 Jan 2025 14:07:38 -0800 Subject: [PATCH 02/64] Update x-axis labels for uptime monitor details chart --- .../Charts/MonitorDetailsAreaChart/index.jsx | 36 ++++++++++++------- Client/src/Pages/Uptime/Details/index.jsx | 5 ++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 2fefd3daa..3256a53d6 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -14,8 +14,14 @@ import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { formatDateWithTz } from "../../../Utils/timeUtils"; import "./index.css"; - -const CustomToolTip = ({ active, payload, label }) => { +// ddd, MMMM D, YYYY, h:mm A +const CustomToolTip = ({ active, payload, label, dateRange }) => { + const formatLookup = { + day: "ddd. MMMM D, YYYY, hh:mm A", + week: "ddd. MMMM D, YYYY, hh:mm A", + month: "ddd. MMMM D, YYYY", + }; + const format = formatLookup[dateRange]; const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); if (active && payload && payload.length) { @@ -41,7 +47,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 formatLookup = { + day: "h:mm A", + week: "MM/D, h:mm A", + month: "ddd. M/D", + }; + const format = formatLookup[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 +138,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 +195,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/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index 7a2aa6756..680a7f690 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -410,7 +410,10 @@ const DetailsPage = () => { Response Times - + Date: Thu, 2 Jan 2025 14:22:31 -0800 Subject: [PATCH 03/64] Refactor date format lookup to util functions, refactor x-axis for infra details charts --- .../Charts/MonitorDetailsAreaChart/index.jsx | 20 +++++-------- .../Charts/Utils/chartUtilFunctions.js | 25 +++++++++++++++++ .../Components/Charts/Utils/chartUtils.jsx | 28 +++++++++++++++---- .../Pages/Infrastructure/Details/index.jsx | 15 ++++++---- 4 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 Client/src/Components/Charts/Utils/chartUtilFunctions.js diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 3256a53d6..91cb1bf22 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -13,15 +13,14 @@ import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { + tooltipDateFormatLookup, + tickDateFormatLookup, +} from "../Utils/chartUtilFunctions"; + import "./index.css"; -// ddd, MMMM D, YYYY, h:mm A const CustomToolTip = ({ active, payload, label, dateRange }) => { - const formatLookup = { - day: "ddd. MMMM D, YYYY, hh:mm A", - week: "ddd. MMMM D, YYYY, hh:mm A", - month: "ddd. MMMM D, YYYY", - }; - const format = formatLookup[dateRange]; + const format = tooltipDateFormatLookup(dateRange); const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); if (active && payload && payload.length) { @@ -111,12 +110,7 @@ CustomToolTip.propTypes = { dateRange: PropTypes.string, }; const CustomTick = ({ x, y, payload, index, dateRange }) => { - const formatLookup = { - day: "h:mm A", - week: "MM/D, h:mm A", - month: "ddd. M/D", - }; - const format = formatLookup[dateRange]; + const format = tickDateFormatLookup(dateRange); const theme = useTheme(); const uiTimezone = useSelector((state) => state.ui.timezone); return ( 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/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: ( ), })) || []), From 6b1d6b64d6a371eff42d7ab5af3129291deec4f1 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 3 Jan 2025 14:04:46 +0000 Subject: [PATCH 04/64] Update material-ui monorepo --- Client/package-lock.json | 94 ++++++++++++++++++++-------------------- Client/package.json | 6 +-- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index c36b48c12..843e964f6 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,9 +11,9 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.3.0", - "@mui/lab": "6.0.0-beta.21", - "@mui/material": "6.3.0", + "@mui/icons-material": "6.3.1", + "@mui/lab": "6.0.0-beta.22", + "@mui/material": "6.3.1", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.23.5", "@mui/x-date-pickers": "7.23.3", @@ -1103,9 +1103,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.0.tgz", - "integrity": "sha512-/d8NwSuC3rMwCjswmGB3oXC4sdDuhIUJ8inVQAxGrADJhf0eq/kmy+foFKvpYhHl2siOZR+MLdFttw6/Bzqtqg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.1.tgz", + "integrity": "sha512-2OmnEyoHpj5//dJJpMuxOeLItCCHdf99pjMFfUFdBteCunAK9jW+PwEo4mtdGcLs7P+IgZ+85ypd52eY4AigoQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1113,9 +1113,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.3.0.tgz", - "integrity": "sha512-3uWws6DveDn5KxCS34p+sUNMxehuclQY6OmoJeJJ+Sfg9L7LGBpksY/nX5ywKAqickTZnn+sQyVcp963ep9jvw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.3.1.tgz", + "integrity": "sha512-nJmWj1PBlwS3t1PnoqcixIsftE+7xrW3Su7f0yrjPw4tVjYrgkhU0hrRp+OlURfZ3ptdSkoBkalee9Bhf1Erfw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1128,7 +1128,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.3.0", + "@mui/material": "^6.3.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1139,16 +1139,16 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-beta.21", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.21.tgz", - "integrity": "sha512-hiFZgTwBNhJMUlEhmqfW4+5wy3C8UF9KFuzSOux6x4kgc9hsC0l+motXcF1Vyh+jhJYGeZ6yUoImqCf9RWzEvw==", + "version": "6.0.0-beta.22", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.22.tgz", + "integrity": "sha512-9nwUfBj+UzoQJOCbqV+JcCSJ74T+gGWrM1FMlXzkahtYUcMN+5Zmh2ArlttW3zv2dZyCzp7K5askcnKF0WzFQg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/base": "5.0.0-beta.68", - "@mui/system": "^6.3.0", - "@mui/types": "^7.2.20", - "@mui/utils": "^6.3.0", + "@mui/system": "^6.3.1", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.3.1", "clsx": "^2.1.1", "prop-types": "^15.8.1" }, @@ -1162,8 +1162,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^6.3.0", - "@mui/material-pigment-css": "^6.3.0", + "@mui/material": "^6.3.1", + "@mui/material-pigment-css": "^6.3.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1184,16 +1184,16 @@ } }, "node_modules/@mui/material": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.0.tgz", - "integrity": "sha512-qhlTFyRMxfoVPxUtA5e8IvqxP0dWo2Ij7cvot7Orag+etUlZH+3UwD8gZGt+3irOoy7Ms3UNBflYjwEikUXtAQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.1.tgz", + "integrity": "sha512-ynG9ayhxgCsHJ/dtDcT1v78/r2GwQyP3E0hPz3GdPRl0uFJz/uUTtI5KFYwadXmbC+Uv3bfB8laZ6+Cpzh03gA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.3.0", - "@mui/system": "^6.3.0", - "@mui/types": "^7.2.20", - "@mui/utils": "^6.3.0", + "@mui/core-downloads-tracker": "^6.3.1", + "@mui/system": "^6.3.1", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.3.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -1212,7 +1212,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.3.0", + "@mui/material-pigment-css": "^6.3.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1239,13 +1239,13 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.0.tgz", - "integrity": "sha512-tdS8jvqMokltNTXg6ioRCCbVdDmZUJZa/T9VtTnX2Lwww3FTgCakst9tWLZSxm1fEE9Xp0m7onZJmgeUmWQYVw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.1.tgz", + "integrity": "sha512-g0u7hIUkmXmmrmmf5gdDYv9zdAig0KoxhIQn1JN8IVqApzf/AyRhH3uDGx5mSvs8+a1zb4+0W6LC260SyTTtdQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.3.0", + "@mui/utils": "^6.3.1", "prop-types": "^15.8.1" }, "engines": { @@ -1266,9 +1266,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.0.tgz", - "integrity": "sha512-iWA6eyiPkO07AlHxRUvI7dwVRSc+84zV54kLmjUms67GJeOWVuXlu8ZO+UhCnwJxHacghxnabsMEqet5PYQmHg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.1.tgz", + "integrity": "sha512-/7CC0d2fIeiUxN5kCCwYu4AWUDd9cCTxWCyo0v/Rnv6s8uk6hWgJC3VLZBoDENBHf/KjqDZuYJ2CR+7hD6QYww==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1300,16 +1300,16 @@ } }, "node_modules/@mui/system": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.0.tgz", - "integrity": "sha512-L+8hUHMNlfReKSqcnVslFrVhoNfz/jw7Fe9NfDE85R3KarvZ4O3MU9daF/lZeqEAvnYxEilkkTfDwQ7qCgJdFg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.1.tgz", + "integrity": "sha512-AwqQ3EAIT2np85ki+N15fF0lFXX1iFPqenCzVOSl3QXKy2eifZeGd9dGtt7pGMoFw5dzW4dRGGzRpLAq9rkl7A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.3.0", - "@mui/styled-engine": "^6.3.0", - "@mui/types": "^7.2.20", - "@mui/utils": "^6.3.0", + "@mui/private-theming": "^6.3.1", + "@mui/styled-engine": "^6.3.1", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.3.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1340,9 +1340,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.20", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz", - "integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==", + "version": "7.2.21", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", + "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1354,13 +1354,13 @@ } }, "node_modules/@mui/utils": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz", - "integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.1.tgz", + "integrity": "sha512-sjGjXAngoio6lniQZKJ5zGfjm+LD2wvLwco7FbKe1fu8A7VIFmz2SwkLb+MDPLNX1lE7IscvNNyh1pobtZg2tw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.20", + "@mui/types": "^7.2.21", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", diff --git a/Client/package.json b/Client/package.json index 75044ea9b..e4853715a 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,9 +14,9 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.3.0", - "@mui/lab": "6.0.0-beta.21", - "@mui/material": "6.3.0", + "@mui/icons-material": "6.3.1", + "@mui/lab": "6.0.0-beta.22", + "@mui/material": "6.3.1", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.23.5", "@mui/x-date-pickers": "7.23.3", From 83dc96887d80ea0d06700b16054ed2f1b52e5432 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 10:31:38 -0800 Subject: [PATCH 05/64] Create a reusable table component --- Client/src/Components/Table/index.jsx | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Client/src/Components/Table/index.jsx diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx new file mode 100644 index 000000000..f8cbdead1 --- /dev/null +++ b/Client/src/Components/Table/index.jsx @@ -0,0 +1,102 @@ +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 }) => { + const theme = useTheme(); + + if ((headers?.length ?? 0) === 0 || (data?.length ?? 0) === 0) { + return "No data"; + } + + return ( + + + + + {headers.map((header, index) => ( + + {header.content} + + ))} + + + + {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, + }).isRequired, +}; + +export default DataTable; From 3d067e8030ed5a0f1aada11c71bd129f3a8c47e3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 10:36:53 -0800 Subject: [PATCH 06/64] make config optional, safe SX access --- Client/src/Components/Table/index.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index f8cbdead1..1edbc3386 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -62,8 +62,8 @@ const DataTable = ({ headers, data, config }) => { return ( config.onRowClick(row)} + sx={config?.rowSX ?? {}} + onClick={() => config?.onRowClick(row)} > {headers.map((header, index) => { return ( @@ -96,7 +96,7 @@ DataTable.propTypes = { config: PropTypes.shape({ onRowClick: PropTypes.func.isRequired, rowSX: PropTypes.object, - }).isRequired, + }), }; export default DataTable; From c192d7024ecc79d6a294d91beac4b7053f49f012 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 11:14:23 -0800 Subject: [PATCH 07/64] Add empty view to table --- Client/src/Components/Table/index.jsx | 52 ++++++++++++++++----------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index 1edbc3386..361d32762 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -35,7 +35,7 @@ import { useTheme } from "@emotion/react"; const DataTable = ({ headers, data, config }) => { const theme = useTheme(); - if ((headers?.length ?? 0) === 0 || (data?.length ?? 0) === 0) { + if ((headers?.length ?? 0) === 0) { return "No data"; } @@ -58,26 +58,37 @@ const DataTable = ({ headers, data, config }) => { - {data.map((row) => { - return ( - config?.onRowClick(row)} + {data.length === 0 ? ( + + - {headers.map((header, index) => { - return ( - - {header.render(row)} - - ); - })} - - ); - })} + {config.emptyView} + + + ) : ( + data.map((row) => { + return ( + config?.onRowClick(row)} + > + {headers.map((header, index) => { + return ( + + {header.render(row)} + + ); + })} + + ); + }) + )} @@ -96,6 +107,7 @@ DataTable.propTypes = { config: PropTypes.shape({ onRowClick: PropTypes.func.isRequired, rowSX: PropTypes.object, + emptyView: PropTypes.node, }), }; From c6ef46133cddb72d6a7c91e538d9da3f6b8165f4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 11:17:55 -0800 Subject: [PATCH 08/64] safe data access, add default empty view --- Client/src/Components/Table/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index 361d32762..570460d30 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -32,7 +32,7 @@ import { useTheme } from "@emotion/react"; * @returns {JSX.Element} The rendered table component. */ -const DataTable = ({ headers, data, config }) => { +const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { const theme = useTheme(); if ((headers?.length ?? 0) === 0) { @@ -58,7 +58,7 @@ const DataTable = ({ headers, data, config }) => { - {data.length === 0 ? ( + {(data?.length ?? 0 === 0) ? ( Date: Fri, 3 Jan 2025 14:30:12 -0500 Subject: [PATCH 09/64] Left align Profile > Password components --- .../src/Components/TabPanels/Account/PasswordPanel.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx index a34c0000d..e5274f76a 100644 --- a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx @@ -126,8 +126,8 @@ const PasswordPanel = () => { noValidate spellCheck="false" gap={theme.spacing(26)} - maxWidth={"80ch"} - marginInline={"auto"} + maxWidth={"80ch"} // Keep maxWidth + > {
@@ -192,7 +192,7 @@ const PasswordPanel = () => { @@ -255,4 +255,4 @@ PasswordPanel.propTypes = { // No props are being passed to this component, hence no specific PropTypes are defined. }; -export default PasswordPanel; +export default PasswordPanel; \ No newline at end of file From 0c608fdc8eadc11b0420143f168ea9fe4bd653fe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 12:09:05 -0800 Subject: [PATCH 10/64] Fix conditional rendering of empty view --- Client/src/Components/Table/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index 570460d30..3cef178f1 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -34,7 +34,6 @@ import { useTheme } from "@emotion/react"; const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { const theme = useTheme(); - if ((headers?.length ?? 0) === 0) { return "No data"; } @@ -58,7 +57,7 @@ const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { - {(data?.length ?? 0 === 0) ? ( + {(data?.length ?? 0) === 0 ? ( Date: Fri, 3 Jan 2025 12:21:33 -0800 Subject: [PATCH 11/64] Use reusable table in team page --- .../TabPanels/Account/TeamPanel.jsx | 103 ++++++++---------- 1 file changed, 46 insertions(+), 57 deletions(-) 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 - From c69389db2c692f52f7fb99ca5dcc655aedd376c5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 12:21:59 -0800 Subject: [PATCH 12/64] use reusable table in incidents page --- .../Pages/Incidents/IncidentTable/index.jsx | 102 ++++++++---------- 1 file changed, 47 insertions(+), 55 deletions(-) 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} )} From c504fbbad6929fb52449b4deb461ed02eb98f0af Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 12:22:24 -0800 Subject: [PATCH 13/64] use reusable table in infrastructure page --- Client/src/Pages/Infrastructure/index.jsx | 237 +++++++++------------- 1 file changed, 92 insertions(+), 145 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index aba4369e6..cf66763cb 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,81 @@ function Infrastructure() { fetchMonitors(); } + const columns = [ + { label: "Host" }, + { label: "Status" }, + { label: "Frequency" }, + { label: "CPU" }, + { label: "Mem" }, + { label: "Disk" }, + { label: "Actions" }, + ]; + + 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 +267,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} + /> Date: Fri, 3 Jan 2025 12:23:08 -0800 Subject: [PATCH 14/64] Use reuseable table in Uptime home page --- .../Home/UptimeTable/Skeleton/index.jsx | 63 ++-- .../Pages/Uptime/Home/UptimeTable/index.jsx | 307 +++++++++--------- 2 files changed, 197 insertions(+), 173 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx index 22d885c97..c17f6d74b 100644 --- a/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx @@ -1,32 +1,47 @@ -import { Skeleton, TableCell, TableRow } from "@mui/material"; +import { Skeleton } from "@mui/material"; +import DataTable from "../../../../../Components/Table"; const ROWS_NUMBER = 7; const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); -const TableBodySkeleton = () => { +const TableSkeleton = () => { /* TODO Skeleton does not follow light and dark theme */ + + const headers = [ + { + id: "name", + + content: "Host", + + render: () => , + }, + { + 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", + }} + /> + ) : ( + + )} Date: Fri, 3 Jan 2025 12:24:06 -0800 Subject: [PATCH 15/64] Move pagination components from Infrastrucutre directory to reusable table directory --- .../Table}/TablePagination/Actions/index.jsx | 8 ++++---- .../Table}/TablePagination/index.jsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename Client/src/{Pages/Infrastructure/components => Components/Table}/TablePagination/Actions/index.jsx (85%) rename Client/src/{Pages/Infrastructure/components => Components/Table}/TablePagination/index.jsx (97%) 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. From 8dd1dc8ccaa5d7b369d98eda40b7087c44d4d29e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 3 Jan 2025 12:41:14 -0800 Subject: [PATCH 16/64] Removed unused BasicTable --- Client/src/Components/BasicTable/index.css | 130 -------- Client/src/Components/BasicTable/index.jsx | 335 --------------------- 2 files changed, 465 deletions(-) delete mode 100644 Client/src/Components/BasicTable/index.css delete mode 100644 Client/src/Components/BasicTable/index.jsx 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; From 09aa62d42ef30924ef666a090582a8e4ab2b61a2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sat, 4 Jan 2025 14:03:58 +0000 Subject: [PATCH 17/64] Update dependency dockerode to v4.0.3 --- Server/package-lock.json | 178 ++++++++++++++++++++++++++++++++++++--- Server/package.json | 2 +- 2 files changed, 168 insertions(+), 12 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 01ca7d510..4baed7570 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -13,7 +13,7 @@ "bcrypt": "5.1.1", "bullmq": "5.34.6", "cors": "^2.8.5", - "dockerode": "4.0.2", + "dockerode": "4.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "handlebars": "^4.7.8", @@ -352,6 +352,37 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", + "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -850,6 +881,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -967,6 +1008,70 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -1132,7 +1237,6 @@ "version": "22.10.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.4.tgz", "integrity": "sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -2654,9 +2758,9 @@ } }, "node_modules/docker-modem": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", - "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.5.tgz", + "integrity": "sha512-Cxw8uEcvNTRmsQuGqzzfiCnfGgf96tVJItLh8taOX0miTcIBALKH5TckCSuZbpbjP7uhAl81dOL9sxfa6HgCIg==", "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", @@ -2669,19 +2773,36 @@ } }, "node_modules/dockerode": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", - "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.3.tgz", + "integrity": "sha512-QSXJFcBQNaGZO6U3qWW4B7p8yRIJn/dWmvL2AQWfO/bjptBBO6QYdVkYSYFz9qoivP2jsOHZfmXMAfrK0BMKyg==", "license": "Apache-2.0", "dependencies": { "@balena/dockerignore": "^1.0.2", - "docker-modem": "^5.0.3", - "tar-fs": "~2.0.1" + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.5", + "protobufjs": "^7.3.2", + "tar-fs": "~2.0.1", + "uuid": "^10.0.0" }, "engines": { "node": ">= 8.0" } }, + "node_modules/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4650,6 +4771,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -4770,6 +4897,12 @@ "node": ">= 12.0.0" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", @@ -6958,6 +7091,30 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8111,7 +8268,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unfetch": { diff --git a/Server/package.json b/Server/package.json index d93c6c640..5ee5e08ad 100644 --- a/Server/package.json +++ b/Server/package.json @@ -20,7 +20,7 @@ "bcrypt": "5.1.1", "bullmq": "5.34.6", "cors": "^2.8.5", - "dockerode": "4.0.2", + "dockerode": "4.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "handlebars": "^4.7.8", From 2a99c87ff21c2d2e4edc7242bb8cd8c4cc7127f3 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:30:39 -0500 Subject: [PATCH 18/64] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 35e3f0fd5..36872f4f0 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ **If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.** -Frame 34 - +![Frame 34](https://github.com/user-attachments/assets/00db1520-24b6-42f2-aca2-a1c9794677da) ![](https://img.shields.io/github/license/bluewave-labs/checkmate) ![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate) From 99089ab103dfda4d9d70e883b88630c3e9b0f70f Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sun, 5 Jan 2025 20:49:23 -0500 Subject: [PATCH 19/64] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36872f4f0..80f654f92 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,8 @@ Here's how you can contribute: Also check other developer and contributor-friendly projects of BlueWave: -- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom. +- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom. - [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform. -- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding), an application that helps new users learn how to use your product via hints, tours, popups and banners. +- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners. - [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform. From a74085abbe4d4e576c814093e158b450be4f0b86 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 6 Jan 2025 04:42:56 -0500 Subject: [PATCH 20/64] Fix screenshot --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 80f654f92..c56280072 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@

Checkmate

-

An open source server and infrastructure monitoring application

+

An open source uptime and infrastructure monitoring application

-![Dashboard-dark](https://github.com/user-attachments/assets/db875138-164f-453c-a75e-889f88747578) +![dashboard](https://github.com/user-attachments/assets/252d6047-522b-4576-8f14-233510e464b8) Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time. From a9ceecbdb2ae994f08d5891b8fc47eda8ae70ff4 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 6 Jan 2025 04:46:28 -0500 Subject: [PATCH 21/64] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c56280072..ba3f51eec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# **We're opening our $5000 grant funding announcement soon, powered by Checkmate and [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.** +# **We're opening our $5000 grant funding announcement soon, in partnership with [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.** **If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.** From e2ce886a16e8ce75c481b393588b80d35322f48e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Jan 2025 10:05:53 -0800 Subject: [PATCH 22/64] Update regex to avoid catastrophic backtracking --- Client/src/Validation/validation.js | 45 +++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index ee4379368..82376d18e 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -95,9 +95,48 @@ const monitorValidation = joi.object({ .string() .trim() .custom((value, helpers) => { - const urlRegex = - /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i; - + var urlRegex = new RegExp( + "^" + + // protocol identifier (optional) + // short syntax // still required + "(?:(?:(?:https?|ftp):)?\\/\\/)" + + // user:pass BasicAuth (optional) + "(?:\\S+(?::\\S*)?@)?" + + "(?:" + + // IP address exclusion + // private & local networks + "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + + "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + + "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + + // IP address dotted notation octets + // excludes loopback network 0.0.0.0 + // excludes reserved space >= 224.0.0.0 + // excludes network & broadcast addresses + // (first & last IP address of each class) + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + + "|" + + // host & domain names, may end with dot + // can be replaced by a shortest alternative + // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ + "(?:" + + "(?:" + + "[a-z0-9\\u00a1-\\uffff]" + + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + + ")?" + + "[a-z0-9\\u00a1-\\uffff]\\." + + ")+" + + // TLD identifier name, may end with dot + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + + ")" + + // port number (optional) + "(?::\\d{2,5})?" + + // resource path (optional) + "(?:[/?#]\\S*)?" + + "$", + "i" + ); if (!urlRegex.test(value)) { return helpers.error("string.invalidUrl"); } From 21a33f8f372d65338e22c8ebb1fe151f3fa635a3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Jan 2025 10:06:26 -0800 Subject: [PATCH 23/64] Added source --- Client/src/Validation/validation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 82376d18e..0138048d8 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -95,6 +95,7 @@ const monitorValidation = joi.object({ .string() .trim() .custom((value, helpers) => { + // Regex from https://gist.github.com/dperini/729294 var urlRegex = new RegExp( "^" + // protocol identifier (optional) From abe2544a2addd93017750df64e39bbc2faaacb3f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Jan 2025 10:13:24 -0800 Subject: [PATCH 24/64] make protocol optional for validation --- Client/src/Validation/validation.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 0138048d8..57d926f6e 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -100,9 +100,8 @@ const monitorValidation = joi.object({ "^" + // protocol identifier (optional) // short syntax // still required - "(?:(?:(?:https?|ftp):)?\\/\\/)" + + "(?:(?:https?|ftp):\\/\\/)?" + // user:pass BasicAuth (optional) - "(?:\\S+(?::\\S*)?@)?" + "(?:" + // IP address exclusion // private & local networks From d60077943273c7fa547c7fd68a28b5f84466d78d Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:57:06 -0500 Subject: [PATCH 25/64] Suggest watching --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba3f51eec..aefa2ebed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # **We're opening our $5000 grant funding announcement soon, in partnership with [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.** -**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.** +**If you would like to support us, please consider giving it a ⭐ and click on "watch" the repo so you can latest news from us. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.** ![Frame 34](https://github.com/user-attachments/assets/00db1520-24b6-42f2-aca2-a1c9794677da) From 208677f9bcc917f0e5ce34a66a3a8ce4d7cae309 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:57:24 -0500 Subject: [PATCH 26/64] fix anyways --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aefa2ebed..b0f158451 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # **We're opening our $5000 grant funding announcement soon, in partnership with [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.** -**If you would like to support us, please consider giving it a ⭐ and click on "watch" the repo so you can latest news from us. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.** +**If you would like to support us, please consider giving it a ⭐ and click on "watch" so you can latest news from us. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.** ![Frame 34](https://github.com/user-attachments/assets/00db1520-24b6-42f2-aca2-a1c9794677da) From ca42e2608f752da9e508cc83aadd62eb69e58682 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Jan 2025 14:24:08 -0800 Subject: [PATCH 27/64] Fix incorrect uptime check count, refactor out query pipelines --- Client/src/Pages/Uptime/Details/index.jsx | 8 +- Server/db/mongo/modules/monitorModule.js | 532 +----------------- .../db/mongo/modules/monitorModuleQueries.js | 525 +++++++++++++++++ 3 files changed, 541 insertions(+), 524 deletions(-) create mode 100644 Server/db/mongo/modules/monitorModuleQueries.js diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index 7a2aa6756..e161027fa 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -308,7 +308,9 @@ const DetailsPage = () => { {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 && ( diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 1143b9eeb..3a511eb12 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -8,7 +8,10 @@ import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUt import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; - +import { + buildUptimeDetailsPipeline, + buildHardwareDetailsPipeline, +} from "./monitorModuleQueries.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -336,272 +339,9 @@ const getUptimeDetailsById = async (req) => { }; const dateString = formatLookup[dateRange]; - const monitorData = await Check.aggregate([ - { - $match: { - monitorId: monitor._id, - }, - }, - { - $sort: { - createdAt: 1, - }, - }, - { - $facet: { - aggregateData: [ - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - firstCheck: { - $first: "$$ROOT", - }, - lastCheck: { - $last: "$$ROOT", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - uptimeDuration: [ - { - $match: { - status: false, - }, - }, - { - $sort: { - createdAt: 1, - }, - }, - { - $group: { - _id: null, - lastFalseCheck: { - $last: "$$ROOT", - }, - }, - }, - ], - groupChecks: [ - { - $match: { - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - { - $sort: { - _id: 1, - }, - }, - ], - groupAggregate: [ - { - $match: { - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - ], - upChecksAggregate: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - upChecks: [ - { - $match: { - status: true, - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - downChecksAggregate: [ - { - $match: { - status: false, - }, - }, - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - downChecks: [ - { - $match: { - status: false, - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - }, - }, - { - $project: { - avgResponseTime: { - $arrayElemAt: ["$aggregateData.avgResponseTime", 0], - }, - totalChecks: { - $arrayElemAt: ["$aggregateData.totalChecks", 0], - }, - latestResponseTime: { - $arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0], - }, - timeSinceLastCheck: { - $let: { - vars: { - lastCheck: { - $arrayElemAt: ["$aggregateData.lastCheck", 0], - }, - }, - in: { - $cond: [ - { - $ifNull: ["$$lastCheck", false], - }, - { - $subtract: [new Date(), "$$lastCheck.createdAt"], - }, - 0, - ], - }, - }, - }, - timeSinceLastFalseCheck: { - $let: { - vars: { - lastFalseCheck: { - $arrayElemAt: ["$uptimeDuration.lastFalseCheck", 0], - }, - firstCheck: { - $arrayElemAt: ["$aggregateData.firstCheck", 0], - }, - }, - in: { - $cond: [ - { - $ifNull: ["$$lastFalseCheck", false], - }, - { - $subtract: [new Date(), "$$lastFalseCheck.createdAt"], - }, - { - $cond: [ - { - $ifNull: ["$$firstCheck", false], - }, - { - $subtract: [new Date(), "$$firstCheck.createdAt"], - }, - 0, - ], - }, - ], - }, - }, - }, - groupChecks: "$groupChecks", - groupAggregate: { - $arrayElemAt: ["$groupAggregate", 0], - }, - upChecksAggregate: { - $arrayElemAt: ["$upChecksAggregate", 0], - }, - upChecks: "$upChecks", - downChecksAggregate: { - $arrayElemAt: ["$downChecksAggregate", 0], - }, - downChecks: "$downChecks", - }, - }, - ]); + const monitorData = await Check.aggregate( + buildUptimeDetailsPipeline(monitor, dates, dateString) + ); const normalizedGroupChecks = NormalizeDataUptimeDetails( monitorData[0].groupChecks, @@ -715,261 +455,9 @@ const getHardwareDetailsById = async (req) => { month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; - const hardwareStats = await HardwareCheck.aggregate([ - { - $match: { - monitorId: monitor._id, - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $sort: { - createdAt: 1, - }, - }, - { - $facet: { - aggregateData: [ - { - $group: { - _id: null, - latestCheck: { - $last: "$$ROOT", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - upChecks: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: null, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - checks: [ - { - $limit: 1, - }, - { - $project: { - diskCount: { - $size: "$disk", - }, - }, - }, - { - $lookup: { - from: "hardwarechecks", - let: { - diskCount: "$diskCount", - }, - pipeline: [ - { - $match: { - $expr: { - $and: [ - { $eq: ["$monitorId", monitor._id] }, - { $gte: ["$createdAt", dates.start] }, - { $lte: ["$createdAt", dates.end] }, - ], - }, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgCpuUsage: { - $avg: "$cpu.usage_percent", - }, - avgMemoryUsage: { - $avg: "$memory.usage_percent", - }, - avgTemperatures: { - $push: { - $ifNull: ["$cpu.temperature", [0]], - }, - }, - disks: { - $push: "$disk", - }, - }, - }, - { - $project: { - _id: 1, - avgCpuUsage: 1, - avgMemoryUsage: 1, - avgTemperature: { - $map: { - input: { - $range: [ - 0, - { - $size: { - // Handle null temperatures array - $ifNull: [ - { $arrayElemAt: ["$avgTemperatures", 0] }, - [0], // Default to single-element array if null - ], - }, - }, - ], - }, - as: "index", - in: { - $avg: { - $map: { - input: "$avgTemperatures", - as: "tempArray", - in: { - $ifNull: [ - { $arrayElemAt: ["$$tempArray", "$$index"] }, - 0, // Default to 0 if element is null - ], - }, - }, - }, - }, - }, - }, - disks: { - $map: { - input: { - $range: [0, "$$diskCount"], - }, - as: "diskIndex", - in: { - name: { - $concat: [ - "disk", - { - $toString: "$$diskIndex", - }, - ], - }, - readSpeed: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: [ - "$$diskArray.read_speed_bytes", - "$$diskIndex", - ], - }, - }, - }, - }, - writeSpeed: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: [ - "$$diskArray.write_speed_bytes", - "$$diskIndex", - ], - }, - }, - }, - }, - totalBytes: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: [ - "$$diskArray.total_bytes", - "$$diskIndex", - ], - }, - }, - }, - }, - freeBytes: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: [ - "$$diskArray.free_bytes", - "$$diskIndex", - ], - }, - }, - }, - }, - usagePercent: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: [ - "$$diskArray.usage_percent", - "$$diskIndex", - ], - }, - }, - }, - }, - }, - }, - }, - }, - }, - ], - as: "hourlyStats", - }, - }, - { - $unwind: "$hourlyStats", - }, - { - $replaceRoot: { - newRoot: "$hourlyStats", - }, - }, - ], - }, - }, - { - $project: { - aggregateData: { - $arrayElemAt: ["$aggregateData", 0], - }, - upChecks: { - $arrayElemAt: ["$upChecks", 0], - }, - checks: { - $sortArray: { - input: "$checks", - sortBy: { _id: 1 }, - }, - }, - }, - }, - ]); + const hardwareStats = await HardwareCheck.aggregate( + buildHardwareDetailsPipeline(monitor, dates, dateString) + ); const monitorStats = { ...monitor.toObject(), 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 }; From 299ae95a0d9841cf3254aa79db21fa22e7036171 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Jan 2025 14:30:48 -0800 Subject: [PATCH 28/64] Add more logging to jobqueue --- Server/service/jobQueue.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 9b5663350..fb5c306b1 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -217,6 +217,23 @@ class NewJobQueue { connection: this.connection, concurrency: 5, }); + + worker.on("active", (job) => { + this.logger.info({ + message: `Worker started processing job: ${job.id}`, + service: SERVICE_NAME, + method: "createWorker", + }); + }); + + worker.on("completed", (job) => { + this.logger.info({ + message: `Worker completed job: ${job.id}`, + service: SERVICE_NAME, + method: "createWorker", + }); + }); + worker.on("failed", (job, err) => { this.logger.error({ message: `Worker failed job: ${job.id}`, @@ -225,6 +242,14 @@ class NewJobQueue { stack: err.stack, }); }); + + worker.on("stalled", (jobId) => { + this.logger.warn({ + message: `Worker stalled job: ${jobId}`, + service: SERVICE_NAME, + method: "createWorker", + }); + }); return worker; } @@ -353,8 +378,9 @@ class NewJobQueue { const jobs = await queue.getJobs(); const ret = await Promise.all( jobs.map(async (job) => { + console.log(job); const state = await job.getState(); - return { url: job.data.url, state }; + return { url: job.data.url, state, progress: job.progress }; }) ); stats[name] = { jobs: ret, workers: this.workers[name].length }; From 9e1f50bc8e023acbab6aa0cd18e3fafe348ff842 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 6 Jan 2025 15:24:16 -0800 Subject: [PATCH 29/64] remove unused var --- Client/src/Pages/Infrastructure/index.jsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index cf66763cb..be7d7e9ed 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -94,16 +94,6 @@ function Infrastructure() { fetchMonitors(); } - const columns = [ - { label: "Host" }, - { label: "Status" }, - { label: "Frequency" }, - { label: "CPU" }, - { label: "Mem" }, - { label: "Disk" }, - { label: "Actions" }, - ]; - const headers = [ { id: "host", From 3ff9b19abf2ddaef6bac6556e3fefdfa066bdd93 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 7 Jan 2025 00:35:23 +0000 Subject: [PATCH 30/64] Update dependency bullmq to v5.34.7 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 4baed7570..8c02aed27 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.6", + "bullmq": "5.34.7", "cors": "^2.8.5", "dockerode": "4.0.3", "dotenv": "^16.4.5", @@ -1744,9 +1744,9 @@ } }, "node_modules/bullmq": { - "version": "5.34.6", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.6.tgz", - "integrity": "sha512-pRCYyO9RlkQWxdmKlrNnUthyFwurYXRYLVXD1YIx+nCCdhAOiHatD8FDHbsT/w2I31c0NWoMcfZiIGuipiF7Lg==", + "version": "5.34.7", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.7.tgz", + "integrity": "sha512-Z6nKi8skD9n5/ErddTngeeEd8dRvZCu8wgZqpeH9zK0DlyTxYKceTnkGab3FkA+U9mzlqa0z34ZKGMbn4GWdgQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/Server/package.json b/Server/package.json index 5ee5e08ad..08cf3e872 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.6", + "bullmq": "5.34.7", "cors": "^2.8.5", "dockerode": "4.0.3", "dotenv": "^16.4.5", From e2d5388a3006d3c058339a253a39fbec6c9fc18c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 10:12:41 -0800 Subject: [PATCH 31/64] remove monitors from summary, unused --- .../src/Pages/Uptime/CreateUptime/index.jsx | 4 +- Client/src/Pages/Uptime/Home/index.jsx | 14 ++-- Server/controllers/monitorController.js | 12 ++-- Server/db/mongo/modules/monitorModule.js | 42 ++++++----- Server/routes/monitorRoute.js | 2 +- Server/service/jobQueue.js | 70 +++++++++++-------- Server/service/settingsService.js | 2 +- Server/validation/joi.js | 8 +-- 8 files changed, 87 insertions(+), 67 deletions(-) diff --git a/Client/src/Pages/Uptime/CreateUptime/index.jsx b/Client/src/Pages/Uptime/CreateUptime/index.jsx index c5a9df770..3499a9c73 100644 --- a/Client/src/Pages/Uptime/CreateUptime/index.jsx +++ b/Client/src/Pages/Uptime/CreateUptime/index.jsx @@ -58,7 +58,7 @@ const CreateMonitor = () => { }; const { user, authToken } = useSelector((state) => state.auth); - const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors); + const { isLoading } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); @@ -205,7 +205,7 @@ const CreateMonitor = () => { } }; fetchMonitor(); - }, [monitorId, authToken, monitors, dispatch, navigate]); + }, [monitorId, authToken, dispatch, navigate]); return ( diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 4fc57a7a0..a542c72ff 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -20,7 +20,7 @@ const UptimeMonitors = () => { const theme = useTheme(); const navigate = useNavigate(); const isAdmin = useIsAdmin(); - const uptimeMonitorsState = useSelector((state) => state.uptimeMonitors); + const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); const authState = useSelector((state) => state.auth); const dispatch = useDispatch({}); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); @@ -35,9 +35,9 @@ const UptimeMonitors = () => { //TODO bring fetching to this component, like on pageSpeed - const loading = uptimeMonitorsState?.isLoading; + const loading = isLoading; - const totalMonitors = uptimeMonitorsState?.monitorsSummary?.monitorCounts?.total; + const totalMonitors = monitorsSummary?.monitorCounts?.total; const hasMonitors = totalMonitors > 0; const noMonitors = !hasMonitors; @@ -86,20 +86,20 @@ const UptimeMonitors = () => { > diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 5e16b3b18..f5e7994b2 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -5,8 +5,8 @@ import { createMonitorBodyValidation, getMonitorURLByQueryValidation, editMonitorBodyValidation, - getMonitorsAndSummaryByTeamIdParamValidation, - getMonitorsAndSummaryByTeamIdQueryValidation, + getMonitorsSummaryByTeamIdParamValidation, + getMonitorsSummaryByTeamIdQueryValidation, getMonitorsByTeamIdQueryValidation, pauseMonitorParamValidation, getMonitorStatsByIdParamValidation, @@ -217,10 +217,10 @@ class MonitorController { * @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team. * @throws {Error} If there is an error during the process, especially if there is a validation error (422). */ - getMonitorsAndSummaryByTeamId = async (req, res, next) => { + getMonitorsSummaryByTeamId = async (req, res, next) => { try { - await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query); + await getMonitorsSummaryByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsSummaryByTeamIdQueryValidation.validateAsync(req.query); } catch (error) { next(handleValidationError(error, SERVICE_NAME)); return; @@ -229,7 +229,7 @@ class MonitorController { try { const { teamId } = req.params; const { type } = req.query; - const monitorsSummary = await this.db.getMonitorsAndSummaryByTeamId(teamId, type); + const monitorsSummary = await this.db.getMonitorsSummaryByTeamId(teamId, type); return res.status(200).json({ success: true, msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 1143b9eeb..6bbb8840a 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -1025,24 +1025,32 @@ const getMonitorById = async (monitorId) => { * @throws {Error} */ -const getMonitorsAndSummaryByTeamId = async (teamId, type) => { +const getMonitorsSummaryByTeamId = async (teamId, type) => { try { - const monitors = await Monitor.find({ teamId, type }); - const monitorCounts = monitors.reduce( - (acc, monitor) => { - if (monitor.status === true) { - acc.up += 1; - } else if (monitor.status === false) { - acc.down += 1; - } else if (monitor.isActive === false) { - acc.paused += 1; - } - return acc; + const monitorCounts = await Monitor.aggregate([ + { + $match: { + type: { $in: type }, + }, }, - { up: 0, down: 0, paused: 0 } - ); - monitorCounts.total = monitors.length; - return { monitors, monitorCounts }; + { + $facet: { + total: [{ $count: "count" }], + up: [{ $match: { status: true } }, { $count: "count" }], + down: [{ $match: { status: false } }, { $count: "count" }], + paused: [{ $match: { isActive: false } }, { $count: "count" }], + }, + }, + { + $project: { + total: { $arrayElemAt: ["$total.count", 0] }, + up: { $arrayElemAt: ["$up.count", 0] }, + down: { $arrayElemAt: ["$down.count", 0] }, + paused: { $arrayElemAt: ["$paused.count", 0] }, + }, + }, + ]); + return { monitorCounts: monitorCounts[0] }; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorsAndSummaryByTeamId"; @@ -1263,7 +1271,7 @@ export { getMonitorStatsById, getMonitorById, getUptimeDetailsById, - getMonitorsAndSummaryByTeamId, + getMonitorsSummaryByTeamId, getMonitorsByTeamId, createMonitor, deleteMonitor, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index d47351486..f3af8e806 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -32,7 +32,7 @@ class MonitorRoutes { this.router.get("/:monitorId", this.monitorController.getMonitorById); this.router.get( "/team/summary/:teamId", - this.monitorController.getMonitorsAndSummaryByTeamId + this.monitorController.getMonitorsSummaryByTeamId ); this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId); diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index fb5c306b1..177ca34c5 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -218,38 +218,50 @@ class NewJobQueue { concurrency: 5, }); - worker.on("active", (job) => { - this.logger.info({ - message: `Worker started processing job: ${job.id}`, - service: SERVICE_NAME, - method: "createWorker", - }); - }); + // worker.on("active", (job) => { + // this.logger.info({ + // message: `Worker started processing job: ${job.id}`, + // service: SERVICE_NAME, + // method: "createWorker", + // }); + // }); - worker.on("completed", (job) => { - this.logger.info({ - message: `Worker completed job: ${job.id}`, - service: SERVICE_NAME, - method: "createWorker", - }); - }); + // worker.on("completed", (job) => { + // this.logger.info({ + // message: `Worker completed job: ${job.id}`, + // service: SERVICE_NAME, + // method: "createWorker", + // }); + // }); - worker.on("failed", (job, err) => { - this.logger.error({ - message: `Worker failed job: ${job.id}`, - service: SERVICE_NAME, - method: "createWorker", - stack: err.stack, - }); - }); + // // Log job progress updates + // worker.on("progress", (job, progress) => { + // this.logger.info({ + // message: `Job progress: ${job.id}`, + // service: SERVICE_NAME, + // method: "createWorker", + // details: `Progress: ${progress}%`, + // }); + // }); - worker.on("stalled", (jobId) => { - this.logger.warn({ - message: `Worker stalled job: ${jobId}`, - service: SERVICE_NAME, - method: "createWorker", - }); - }); + // // Log when a job fails + // worker.on("failed", (job, err) => { + // this.logger.error({ + // message: `Worker failed job: ${job.id}`, + // service: SERVICE_NAME, + // method: "createWorker", + // details: `Error: ${err.message}`, + // stack: err.stack, + // }); + // }); + + // worker.on("stalled", (jobId) => { + // this.logger.warn({ + // message: `Worker stalled job: ${jobId}`, + // service: SERVICE_NAME, + // method: "createWorker", + // }); + // }); return worker; } diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js index fb85939ca..24931f6ce 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -2,7 +2,7 @@ const SERVICE_NAME = "SettingsService"; import dotenv from "dotenv"; dotenv.config(); const envConfig = { - logLevel: undefined, + logLevel: process.env.LOG_LEVEL, apiBaseUrl: undefined, clientHost: process.env.CLIENT_HOST, jwtSecret: process.env.JWT_SECRET, diff --git a/Server/validation/joi.js b/Server/validation/joi.js index dbffef937..825c90cdb 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -136,11 +136,11 @@ const getMonitorByIdQueryValidation = joi.object({ normalize: joi.boolean(), }); -const getMonitorsAndSummaryByTeamIdParamValidation = joi.object({ +const getMonitorsSummaryByTeamIdParamValidation = joi.object({ teamId: joi.string().required(), }); -const getMonitorsAndSummaryByTeamIdQueryValidation = joi.object({ +const getMonitorsSummaryByTeamIdQueryValidation = joi.object({ type: joi .alternatives() .try( @@ -467,8 +467,8 @@ export { createMonitorBodyValidation, getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsAndSummaryByTeamIdParamValidation, - getMonitorsAndSummaryByTeamIdQueryValidation, + getMonitorsSummaryByTeamIdParamValidation, + getMonitorsSummaryByTeamIdQueryValidation, getMonitorsByTeamIdValidation, getMonitorsByTeamIdQueryValidation, getMonitorStatsByIdParamValidation, From de513c226c70cff36e69c26a72e4952a944844da Mon Sep 17 00:00:00 2001 From: Kalyan Bhowmick <“kbhowmick067@gmail.com”> Date: Tue, 7 Jan 2025 23:48:12 +0530 Subject: [PATCH 32/64] Query updated for Incidents page filter --- Server/db/mongo/modules/checkModule.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 773d6ef1e..f8c1a7581 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -175,9 +175,10 @@ const getTeamChecks = async (req) => { case "all": break; case "down": + checksQuery.status = false; break; case "resolve": - checksQuery.statusCode = 5000; + checksQuery.status = true; break; default: logger.warn({ From 290740d58e2e27e6dfb4c34bc3a7cf82f928b0db Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:24:47 -0800 Subject: [PATCH 33/64] Add getting monitors by team id to redux slice to make use of loading state --- .../UptimeMonitors/uptimeMonitorsSlice.js | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 78250c19a..8782948ae 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -78,12 +78,12 @@ export const getUptimeMonitorById = createAsyncThunk( } ); -export const getUptimeMonitorsByTeamId = createAsyncThunk( - "monitors/getMonitorsByTeamId", +export const getUptimeSummaryByTeamId = createAsyncThunk( + "monitors/getSummaryByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsAndSummaryByTeamId({ + const res = await networkService.getMonitorsSummaryByTeamId({ authToken: token, teamId: user.teamId, types: ["http", "ping", "docker", "port"], @@ -102,6 +102,38 @@ export const getUptimeMonitorsByTeamId = createAsyncThunk( } ); +export const getUptimeMonitorsByTeamId = createAsyncThunk( + "monitors/getMonitorsByTeamId", + async (config, thunkApi) => { + try { + const res = await networkService.getMonitorsByTeamId({ + authToken: config.authToken, + teamId: config.teamId, + limit: 25, + types: ["http", "ping", "docker", "port"], + status: null, + checkOrder: "desc", + normalize: true, + page: config.page, + rowsPerPage: config.rowsPerPage, + filter: config.filter, + field: config.sort.field, + order: config.sort.order, + }); + 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 updateUptimeMonitor = createAsyncThunk( "monitors/updateMonitor", async (data, thunkApi) => { @@ -258,16 +290,33 @@ const uptimeMonitorsSlice = createSlice({ extraReducers: (builder) => { builder // ***************************************************** - // Monitors by teamId + // Summary by teamId // ***************************************************** + .addCase(getUptimeSummaryByTeamId.pending, (state) => { + state.isLoading = true; + }) + .addCase(getUptimeSummaryByTeamId.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.msg; + state.monitorsSummary = action.payload.data; + }) + .addCase(getUptimeSummaryByTeamId.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload ? action.payload.msg : "Getting uptime summary failed"; + }) + + // ***************************************************** + // Monitors by teamId + // ***************************************************** .addCase(getUptimeMonitorsByTeamId.pending, (state) => { state.isLoading = true; }) .addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => { state.isLoading = false; - state.success = action.payload.msg; - state.monitorsSummary = action.payload.data; + state.success = action.payload.success; + state.msg = action.payload.msg; }) .addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => { state.isLoading = false; @@ -276,7 +325,6 @@ const uptimeMonitorsSlice = createSlice({ ? action.payload.msg : "Getting uptime monitors failed"; }) - // ***************************************************** // Create Monitor // ***************************************************** From 33b3143298a298b8b00f33a8badedd88e581b657 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:27:55 -0800 Subject: [PATCH 34/64] use correct name for redux action --- Client/src/Pages/Uptime/Configure/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Uptime/Configure/index.jsx b/Client/src/Pages/Uptime/Configure/index.jsx index 32532d462..76c030776 100644 --- a/Client/src/Pages/Uptime/Configure/index.jsx +++ b/Client/src/Pages/Uptime/Configure/index.jsx @@ -11,7 +11,7 @@ import { updateUptimeMonitor, pauseUptimeMonitor, getUptimeMonitorById, - getUptimeMonitorsByTeamId, + getUptimeSummaryByTeamId, deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import TextInput from "../../../Components/Inputs/TextInput"; @@ -157,7 +157,7 @@ const Configure = () => { const action = await dispatch(updateUptimeMonitor({ authToken, monitor: monitor })); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor updated successfully!" }); - dispatch(getUptimeMonitorsByTeamId(authToken)); + dispatch(getUptimeSummaryByTeamId(authToken)); } else { createToast({ body: "Failed to update monitor." }); } From 37207da9fa58034a6acb71a778260a394ce5749f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:28:46 -0800 Subject: [PATCH 35/64] Rename get method in network service --- Client/src/Utils/NetworkService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 4812947d0..cc70b0f69 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -127,7 +127,7 @@ class NetworkService { * @param {Array} config.types - Array of monitor types * @returns {Promise} The response from the axios POST request. */ - async getMonitorsAndSummaryByTeamId(config) { + async getMonitorsSummaryByTeamId(config) { const params = new URLSearchParams(); if (config.types) { From 8bc05e442480ed349772cd1063b7e1a35e7ee8e4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:29:47 -0800 Subject: [PATCH 36/64] Rewrite uptime home page for simplicity, move queries to parent --- Client/src/Pages/Uptime/Home/index.jsx | 218 ++++++++++++++++++++----- 1 file changed, 180 insertions(+), 38 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index a542c72ff..616cfeeee 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -1,46 +1,180 @@ -import "./index.css"; -import { useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { Box, Button, Stack } from "@mui/material"; -import PropTypes from "prop-types"; +// Components +import { Box, Stack, Button } from "@mui/material"; +import Greeting from "../../../Utils/greeting"; import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; import StatusBox from "./StatusBox"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import Greeting from "../../../Utils/greeting"; -import { CurrentMonitoring } from "./CurrentMonitoring"; +import UptimeDataTable from "./UptimeDataTable"; +import { Pagination } from "../../../Components/Table/TablePagination"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useEffect, useState, useCallback, useMemo, useRef } from "react"; +import { + getUptimeSummaryByTeamId, + getUptimeMonitorsByTeamId, +} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useSelector, useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../Utils/toastUtils"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import useDebounce from "../../../Utils/debounce"; const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; +const useWhyDidYouUpdate = (name, props) => { + const previousProps = useRef(); + + useEffect(() => { + if (previousProps.current) { + const allKeys = Object.keys({ ...previousProps.current, ...props }); + const changesObj = {}; + + allKeys.forEach((key) => { + if (previousProps.current[key] !== props[key]) { + changesObj[key] = { + from: previousProps.current[key], + to: props[key], + }; + } + }); + + if (Object.keys(changesObj).length) { + console.log("[why-did-you-update]", name, changesObj); + } + } + + previousProps.current = props; + }); +}; + const UptimeMonitors = () => { + // Redux state + const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); + const authState = useSelector((state) => state.auth); + const { rowsPerPage } = useSelector((state) => state.ui.monitors); + + // Local state + const [monitors, setMonitors] = useState([]); + const [sort, setSort] = useState({}); + const [search, setSearch] = useState(""); + const [page, setPage] = useState(0); + const [isSearching, setIsSearching] = useState(false); + const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); + + // Utils + const debouncedFilter = useDebounce(search, 500); + const dispatch = useDispatch(); const theme = useTheme(); const navigate = useNavigate(); const isAdmin = useIsAdmin(); - const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); - const authState = useSelector((state) => state.auth); - const dispatch = useDispatch({}); - const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const handlePause = () => { + const fetchParams = useMemo( + () => ({ + authToken: authState.authToken, + teamId: authState.user.teamId, + sort: { field: sort.field, order: sort.order }, + filter: debouncedFilter, + page, + rowsPerPage, + }), + [authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage] + ); + + useWhyDidYouUpdate("UptimeMonitors", { + isLoading, + monitorsSummary, + authState, + rowsPerPage, + monitors, + sort, + search, + page, + isSearching, + monitorUpdateTrigger, + fetchParams, + }); + + const getMonitorWithPercentage = useCallback((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, + }; + }, []); + + const fetchMonitors = useCallback(async () => { + try { + const action = await dispatch(getUptimeMonitorsByTeamId(fetchParams)); + if (action.payload.success) { + const { monitors } = action.payload.data; + const mappedMonitors = monitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(mappedMonitors); + } else { + // TODO: Check for other errors? + throw new Error("Error fetching monitors"); + } + } catch (error) { + createToast({ + body: "Error fetching monitors", + }); + } finally { + setIsSearching(false); + } + }, [fetchParams, dispatch, getMonitorWithPercentage, theme]); + + useEffect(() => { + dispatch(getUptimeSummaryByTeamId(authState.authToken)); + fetchMonitors(); + }, [fetchMonitors, monitorUpdateTrigger, authState.authToken, dispatch]); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + dispatch( + setRowsPerPage({ + value: parseInt(event.target.value, 10), + table: "monitors", + }) + ); + setPage(0); + }; + + const triggerUpdate = () => { setMonitorUpdateTrigger((prev) => !prev); }; - useEffect(() => { - dispatch(getUptimeMonitorsByTeamId(authState.authToken)); - }, [authState.authToken, dispatch, monitorUpdateTrigger]); - - //TODO bring fetching to this component, like on pageSpeed - - const loading = isLoading; - const totalMonitors = monitorsSummary?.monitorCounts?.total; - const hasMonitors = totalMonitors > 0; - const noMonitors = !hasMonitors; const canAddMonitor = isAdmin && hasMonitors; return ( @@ -72,11 +206,9 @@ const UptimeMonitors = () => { - {loading ? ( - - ) : ( + { <> - {noMonitors && } + {!isLoading && !hasMonitors && } {hasMonitors && ( <> { value={monitorsSummary?.monitorCounts?.paused ?? 0} /> - + )} - )} + } ); }; -UptimeMonitors.propTypes = { - isAdmin: PropTypes.bool, -}; export default UptimeMonitors; From e070ef472eebb17ffcf1f747bbe81b1a77778b31 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:30:27 -0800 Subject: [PATCH 37/64] Create new UptimeDataTable component to hold search, table, and pagination --- .../Home/UptimeDataTable/Skeleton/index.jsx | 47 ++++ .../Uptime/Home/UptimeDataTable/index.jsx | 231 ++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx new file mode 100644 index 000000000..c17f6d74b --- /dev/null +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx @@ -0,0 +1,47 @@ +import { Skeleton } from "@mui/material"; +import DataTable from "../../../../../Components/Table"; +const ROWS_NUMBER = 7; +const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); + +const 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 ( + + ); +}; + +export { TableSkeleton }; diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx new file mode 100644 index 000000000..4739b37de --- /dev/null +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -0,0 +1,231 @@ +// Components +import { Box, Stack, CircularProgress } from "@mui/material"; +import Search from "../../../../Components/Inputs/Search"; +import { Heading } from "../../../../Components/Heading"; +import DataTable from "../../../../Components/Table"; +import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; +import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import Host from "../host"; +import { StatusLabel } from "../../../../Components/Label"; +import BarChart from "../../../../Components/Charts/BarChart"; +import ActionsMenu from "../actionsMenu"; + +// Utils +import { useTheme } from "@emotion/react"; +import useDebounce from "../../../../Utils/debounce"; +import { useState } from "react"; +import useUtils from "../../utils"; +import { memo } from "react"; +import { useNavigate } from "react-router-dom"; + +const UptimeDataTable = ({ + isAdmin, + isLoading, + monitors, + monitorCount, + sort, + setSort, + search, + setSearch, + isSearching, + setIsSearching, + triggerUpdate, +}) => { + const { determineState } = useUtils(); + + const theme = useTheme(); + const navigate = useNavigate(); + + //Utils + const debouncedFilter = useDebounce(search, 500); + const handleSearch = (value) => { + setIsSearching(true); + setSearch(value); + }; + + const handleSort = (field) => { + let order = ""; + if (sort.field !== field) { + order = "desc"; + } else { + order = sort.order === "asc" ? "desc" : "asc"; + } + setSort({ field, order }); + }; + + 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) => ( + + ), + }, + ]; + + return ( + + + Uptime monitors + + + {monitorCount} + + + + + + + {isSearching && ( + <> + + + + + + )} + { + navigate(`/uptime/${row.id}`); + }, + emptyView: "No monitors found", + }} + /> + + + ); +}; + +const MemoizedUptimeDataTable = memo(UptimeDataTable); +export default MemoizedUptimeDataTable; From 89d0a74ade40c159e6cf80d20bdc5ef67c918750 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:31:25 -0800 Subject: [PATCH 38/64] Use new table component, remove old table --- .../Uptime/Home/CurrentMonitoring/index.jsx | 71 ---- .../Home/UptimeTable/Skeleton/index.jsx | 47 --- .../Pages/Uptime/Home/UptimeTable/index.jsx | 345 ------------------ Client/src/Pages/Uptime/Home/actionsMenu.jsx | 4 +- Client/src/Routes/index.jsx | 1 + 5 files changed, 4 insertions(+), 464 deletions(-) delete mode 100644 Client/src/Pages/Uptime/Home/CurrentMonitoring/index.jsx delete mode 100644 Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx delete mode 100644 Client/src/Pages/Uptime/Home/UptimeTable/index.jsx diff --git a/Client/src/Pages/Uptime/Home/CurrentMonitoring/index.jsx b/Client/src/Pages/Uptime/Home/CurrentMonitoring/index.jsx deleted file mode 100644 index 2d9426137..000000000 --- a/Client/src/Pages/Uptime/Home/CurrentMonitoring/index.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { Box, Stack } from "@mui/material"; -import Search from "../../../../Components/Inputs/Search"; -import MemoizedMonitorTable from "../UptimeTable"; -import { useState } from "react"; -import useDebounce from "../../../../Utils/debounce"; -import PropTypes from "prop-types"; -import { Heading } from "../../../../Components/Heading"; - -const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin, handlePause }) => { - const theme = useTheme(); - const [search, setSearch] = useState(""); - const [isSearching, setIsSearching] = useState(false); - const debouncedFilter = useDebounce(search, 500); - const handleSearch = (value) => { - setIsSearching(true); - setSearch(value); - }; - return ( - - - Uptime monitors - - - {totalMonitors} - - - - - - - - ); -}; - -CurrentMonitoring.propTypes = { - handlePause: PropTypes.func, - totalMonitors: PropTypes.number, - monitors: PropTypes.array, - isAdmin: PropTypes.bool, -}; - -export { CurrentMonitoring }; diff --git a/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx deleted file mode 100644 index c17f6d74b..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Skeleton } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -const ROWS_NUMBER = 7; -const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); - -const 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 ( - - ); -}; - -export { TableSkeleton }; diff --git a/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx deleted file mode 100644 index 0aa0eef90..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx +++ /dev/null @@ -1,345 +0,0 @@ -import PropTypes from "prop-types"; -import { useState, useEffect, memo, useCallback, useRef } from "react"; -import { useNavigate } from "react-router"; -import { useDispatch, useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import useUtils from "../../utils"; - -import { setRowsPerPage } from "../../../../Features/UI/uiSlice"; -import { logger } from "../../../../Utils/Logger"; -import { jwtDecode } from "jwt-decode"; -import { networkService } from "../../../../main"; - -import { Box, CircularProgress } from "@mui/material"; -import ActionsMenu from "../actionsMenu"; -import Host from "../host"; -import { StatusLabel } from "../../../../Components/Label"; -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 "../../../../Components/Table/TablePagination"; -import DataTable from "../../../../Components/Table"; - -const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePause }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const { determineState } = useUtils(); - - const { rowsPerPage } = useSelector((state) => state.ui.monitors); - const authState = useSelector((state) => state.auth); - - const [page, setPage] = useState(0); - const [monitors, setMonitors] = useState([]); - 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); - }; - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "monitors", - }) - ); - setPage(0); - }; - - const fetchPage = useCallback(async () => { - try { - const { authToken } = authState; - const user = jwtDecode(authToken); - const res = await networkService.getMonitorsByTeamId({ - authToken, - teamId: user.teamId, - limit: 25, - types: ["http", "ping", "docker", "port"], - status: null, - checkOrder: "desc", - normalize: true, - page: page, - rowsPerPage: rowsPerPage, - filter: filter, - field: sort.field, - order: sort.order, - }); - setMonitors(res?.data?.data?.monitors ?? []); - setMonitorCount(res?.data?.data?.monitorCount ?? 0); - } catch (error) { - logger.error(error); - } finally { - setIsSearching(false); - } - }, [authState, page, rowsPerPage, filter, sort, setIsSearching]); - - useEffect(() => { - fetchPage(); - }, [ - updateTrigger, - authState, - page, - rowsPerPage, - filter, - sort, - setIsSearching, - fetchPage, - ]); - - // Listen for changes in filter, if new value reset the page - useEffect(() => { - if (prevFilter.current !== filter) { - setPage(0); - fetchPage(); - } - prevFilter.current = filter; - }, [filter, fetchPage]); - - const handleSort = async (field) => { - let order = ""; - if (sort.field !== field) { - order = "desc"; - } else { - order = sort.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); - - const { authToken } = authState; - const user = jwtDecode(authToken); - - const res = await networkService.getMonitorsByTeamId({ - authToken, - teamId: user.teamId, - limit: 25, - types: ["http", "ping"], - status: null, - checkOrder: "desc", - normalize: true, - page: page, - rowsPerPage: rowsPerPage, - filter: null, - field: field, - order: order, - }); - setMonitors(res?.data?.data?.monitors ?? []); - setMonitorCount(res?.data?.data?.monitorCount ?? 0); - }; - - 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 && ( - <> - - - - - - )} - {/* - 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", - }} - /> - ) : ( - - )} - - - ); -}; - -MonitorTable.propTypes = { - isAdmin: PropTypes.bool, - filter: PropTypes.string, - setIsSearching: PropTypes.func, - isSearching: PropTypes.bool, - setMonitorUpdateTrigger: PropTypes.func, - handlePause: PropTypes.func, -}; - -const MemoizedMonitorTable = memo(MonitorTable); -export default MemoizedMonitorTable; diff --git a/Client/src/Pages/Uptime/Home/actionsMenu.jsx b/Client/src/Pages/Uptime/Home/actionsMenu.jsx index 01452b718..bda27bf15 100644 --- a/Client/src/Pages/Uptime/Home/actionsMenu.jsx +++ b/Client/src/Pages/Uptime/Home/actionsMenu.jsx @@ -34,7 +34,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) => if (action.meta.requestStatus === "fulfilled") { setIsOpen(false); // close modal dispatch(getUptimeMonitorsByTeamId(authState.authToken)); - updateCallback(); + updateRowCallback(); createToast({ body: "Monitor deleted successfully." }); } else { createToast({ body: "Failed to delete monitor." }); @@ -169,6 +169,8 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) => {isAdmin && ( { + closeMenu(e); + e.stopPropagation(); handlePause(e); }} diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index 7d444f2ab..b5f4dd376 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -48,6 +48,7 @@ const Routes = () => { path="/uptime" element={} /> + } From 18988807749a59f6c3f7e112da4e9c313ae3a7a5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:34:18 -0800 Subject: [PATCH 39/64] remove utility hook --- Client/src/Pages/Uptime/Home/index.jsx | 40 -------------------------- 1 file changed, 40 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 616cfeeee..9a2c7784a 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -24,32 +24,6 @@ import useDebounce from "../../../Utils/debounce"; const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; -const useWhyDidYouUpdate = (name, props) => { - const previousProps = useRef(); - - useEffect(() => { - if (previousProps.current) { - const allKeys = Object.keys({ ...previousProps.current, ...props }); - const changesObj = {}; - - allKeys.forEach((key) => { - if (previousProps.current[key] !== props[key]) { - changesObj[key] = { - from: previousProps.current[key], - to: props[key], - }; - } - }); - - if (Object.keys(changesObj).length) { - console.log("[why-did-you-update]", name, changesObj); - } - } - - previousProps.current = props; - }); -}; - const UptimeMonitors = () => { // Redux state const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); @@ -83,20 +57,6 @@ const UptimeMonitors = () => { [authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage] ); - useWhyDidYouUpdate("UptimeMonitors", { - isLoading, - monitorsSummary, - authState, - rowsPerPage, - monitors, - sort, - search, - page, - isSearching, - monitorUpdateTrigger, - fetchParams, - }); - const getMonitorWithPercentage = useCallback((monitor, theme) => { let uptimePercentage = ""; let percentageColor = theme.palette.percentage.uptimeExcellent; From 0cc5c732c7cf6fd67541ff57a1e2b61eafc8a7c3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:45:18 -0800 Subject: [PATCH 40/64] Fix sort arrow positioning --- .../Uptime/Home/UptimeDataTable/index.jsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx index 4739b37de..d174c535f 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -17,6 +17,7 @@ import { useState } from "react"; import useUtils from "../../utils"; import { memo } from "react"; import { useNavigate } from "react-router-dom"; +import "../index.css"; const UptimeDataTable = ({ isAdmin, @@ -57,9 +58,15 @@ const UptimeDataTable = ({ { id: "name", content: ( - handleSort("name")}> + handleSort("name")} + > Host - )} - - + + ), render: (row) => ( handleSort("status")} > {" "} Status - )} - - + + ), render: (row) => { const status = determineState(row.monitor); From 787bb092354096bb453967d4a038e4da3347949b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 14:52:10 -0800 Subject: [PATCH 41/64] fix pagination icon positioning --- Client/src/Components/Table/TablePagination/index.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Client/src/Components/Table/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx index 4435454bc..a2c7ad067 100644 --- a/Client/src/Components/Table/TablePagination/index.jsx +++ b/Client/src/Components/Table/TablePagination/index.jsx @@ -94,6 +94,13 @@ function Pagination({ "&.Mui-focused > div": { backgroundColor: theme.palette.background.main, }, + "& .MuiSelect-icon": { + // Add this style override + position: "absolute", + right: 0, + top: "50%", + transform: "translateY(-50%)", + }, }, }, }} From 0b803da5fb93d2242d4451bb1cac20b0afd3df26 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 7 Jan 2025 16:27:35 -0800 Subject: [PATCH 42/64] rewrite query to use aggregation pipeline --- Server/db/mongo/modules/monitorModule.js | 144 ++++++++++++++++++----- Server/utils/dataUtils.js | 3 +- 2 files changed, 115 insertions(+), 32 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index c296de34b..84c7ff971 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -12,6 +12,7 @@ import { buildUptimeDetailsPipeline, buildHardwareDetailsPipeline, } from "./monitorModuleQueries.js"; +import { ObjectId } from "mongodb"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -29,6 +30,15 @@ const CHECK_MODEL_LOOKUP = { hardware: HardwareCheck, }; +const MODEL_TYPE_MAPPING = { + http: "checks", + ping: "checks", + docker: "checks", + port: "checks", + pagespeed: "pagespeedchecks", + hardware: "hardwarechecks", +}; + /** * Get all monitors * @async @@ -570,6 +580,7 @@ const getMonitorsByTeamId = async (req, res) => { } = req.query; const monitorQuery = { teamId: req.params.teamId }; + const monitorCount = await Monitor.countDocuments(monitorQuery); if (type !== undefined) { monitorQuery.type = Array.isArray(type) ? { $in: type } : type; @@ -582,46 +593,117 @@ const getMonitorsByTeamId = async (req, res) => { { url: { $regex: filter, $options: "i" } }, ]; } - const monitorCount = await Monitor.countDocuments(monitorQuery); // Pagination const skip = page && rowsPerPage ? page * rowsPerPage : 0; // Build Sort option - const sort = field ? { [field]: order === "asc" ? 1 : -1 } : {}; + const sort = { [field]: order === "asc" ? 1 : -1 }; - const monitors = await Monitor.find(monitorQuery) - .skip(skip) - .limit(rowsPerPage) - .sort(sort); - - // Early return if limit is set to -1, indicating we don't want any checks - if (limit === "-1") { - return { monitors, monitorCount }; + const matchStage = { teamId: new ObjectId(req.params.teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + if (filter !== undefined) { + matchStage.$or = [ + { name: { $regex: filter, $options: "i" } }, + { url: { $regex: filter, $options: "i" } }, + ]; } - // Map each monitor to include its associated checks - const monitorsWithChecks = await Promise.all( - monitors.map(async (monitor) => { - let model = CHECK_MODEL_LOOKUP[monitor.type]; - // Checks are order newest -> oldest - let checks = await model - .find({ - monitorId: monitor._id, - ...(status && { status }), - }) - .sort({ createdAt: checkOrder === "asc" ? 1 : -1 }) + let result = await Monitor.aggregate([ + { $match: matchStage }, + { $skip: parseInt(skip) }, + ...(rowsPerPage ? [{ $limit: parseInt(rowsPerPage) }] : []), + { $sort: sort }, + { + $lookup: { + from: "checks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + ...(status && { status }), + }, + }, + { $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } }, + { $limit: parseInt(limit) || 0 }, + ], + as: "standardchecks", + }, + }, + { + $lookup: { + from: "pagespeedchecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + ...(status && { status }), + }, + }, + { $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } }, + { $limit: parseInt(limit) || 0 }, + ], + as: "pagespeedchecks", + }, + }, + { + $lookup: { + from: "hardwarechecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + ...(status && { status }), + }, + }, + { $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } }, + { $limit: parseInt(limit) || 0 }, + ], + as: "hardwarechecks", + }, + }, + { + $addFields: { + checks: { + $switch: { + branches: [ + { + case: { $in: ["$type", ["http", "ping", "docker", "port"]] }, + then: "$standardchecks", + }, + { + case: { $eq: ["$type", "pagespeed"] }, + then: "$pagespeedchecks", + }, + { + case: { $eq: ["$type", "hardware"] }, + then: "$hardwarechecks", + }, + ], + default: [], + }, + }, + }, + }, + { + $project: { + standardchecks: 0, + pagespeedchecks: 0, + hardwarechecks: 0, + }, + }, + ]); + result = result.map((monitor) => { + monitor.checks = NormalizeData(monitor.checks, 10, 100); + return monitor; + }); - .limit(limit || 0); - - //Normalize checks if requested - if (normalize !== undefined) { - checks = NormalizeData(checks, 10, 100); - } - return { ...monitor.toObject(), checks }; - }) - ); - return { monitors: monitorsWithChecks, monitorCount }; + return { monitors: result, monitorCount }; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorsByTeamId"; diff --git a/Server/utils/dataUtils.js b/Server/utils/dataUtils.js index fd10a4257..8fbb22821 100644 --- a/Server/utils/dataUtils.js +++ b/Server/utils/dataUtils.js @@ -26,6 +26,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { const min = calculatePercentile(checks, 0); const max = calculatePercentile(checks, 95); const normalizedChecks = checks.map((check) => { + console.log(check); const originalResponseTime = check.responseTime; // Normalize the response time between 1 and 100 let normalizedResponseTime = @@ -38,7 +39,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { Math.min(rangeMax, normalizedResponseTime) ); return { - ...check._doc, + ...check, responseTime: normalizedResponseTime, originalResponseTime: originalResponseTime, }; From 0e4cf8cded5e2cbbf882a0f662019434987b6c98 Mon Sep 17 00:00:00 2001 From: Kalyan Bhowmick <“kbhowmick067@gmail.com”> Date: Wed, 8 Jan 2025 10:30:22 +0530 Subject: [PATCH 43/64] resolve query reverted --- Server/db/mongo/modules/checkModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index f8c1a7581..0a552012d 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -178,7 +178,7 @@ const getTeamChecks = async (req) => { checksQuery.status = false; break; case "resolve": - checksQuery.status = true; + checksQuery.statusCode = 5000; break; default: logger.warn({ From dffbce1de1a9c31647a0517b6ef901ad0bc6c5d4 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 8 Jan 2025 05:48:23 +0000 Subject: [PATCH 44/64] Update dependency bullmq to v5.34.8 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 8c02aed27..30306bf29 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.7", + "bullmq": "5.34.8", "cors": "^2.8.5", "dockerode": "4.0.3", "dotenv": "^16.4.5", @@ -1744,9 +1744,9 @@ } }, "node_modules/bullmq": { - "version": "5.34.7", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.7.tgz", - "integrity": "sha512-Z6nKi8skD9n5/ErddTngeeEd8dRvZCu8wgZqpeH9zK0DlyTxYKceTnkGab3FkA+U9mzlqa0z34ZKGMbn4GWdgQ==", + "version": "5.34.8", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.8.tgz", + "integrity": "sha512-id5mmPg3K8tNXQ9VVlmUxBSeLmliIWUrB8Hd5c62PFrIiHywz4TN1PEqU6OWvYXEvoFCr8/BlnbE4JCrGqPVmg==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/Server/package.json b/Server/package.json index 08cf3e872..222f95f16 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.7", + "bullmq": "5.34.8", "cors": "^2.8.5", "dockerode": "4.0.3", "dotenv": "^16.4.5", From dc43c0486c6de95b70c5fa2842e343518a9c4f26 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Wed, 8 Jan 2025 01:22:33 -0500 Subject: [PATCH 45/64] Add OpenSSF best practices badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b0f158451..8e9f34fb7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ ![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate) ![](https://img.shields.io/github/issues/bluewave-labs/checkmate) ![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901) +

Checkmate

From ef56283fa9547e8cecc6bed1ac2ef245f308df8f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 10:05:34 -0800 Subject: [PATCH 46/64] Fix merge conflict, remove unused dispatch --- Client/src/Pages/Uptime/Configure/index.jsx | 2 -- Client/src/Pages/Uptime/Home/index.jsx | 8 +------- Server/db/mongo/modules/monitorModule.js | 9 --------- Server/utils/dataUtils.js | 1 - 4 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Client/src/Pages/Uptime/Configure/index.jsx b/Client/src/Pages/Uptime/Configure/index.jsx index 76c030776..6cef97e0f 100644 --- a/Client/src/Pages/Uptime/Configure/index.jsx +++ b/Client/src/Pages/Uptime/Configure/index.jsx @@ -11,7 +11,6 @@ import { updateUptimeMonitor, pauseUptimeMonitor, getUptimeMonitorById, - getUptimeSummaryByTeamId, deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import TextInput from "../../../Components/Inputs/TextInput"; @@ -157,7 +156,6 @@ const Configure = () => { const action = await dispatch(updateUptimeMonitor({ authToken, monitor: monitor })); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor updated successfully!" }); - dispatch(getUptimeSummaryByTeamId(authToken)); } else { createToast({ body: "Failed to update monitor." }); } diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 50ab2572b..7e1f056ca 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -14,7 +14,6 @@ import { getUptimeSummaryByTeamId, getUptimeMonitorsByTeamId, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useSelector, useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; @@ -26,15 +25,13 @@ const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; const UptimeMonitors = () => { // Redux state - const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); - const authState = useSelector((state) => state.auth); - const { rowsPerPage } = useSelector((state) => state.ui.monitors); // Local state const [monitors, setMonitors] = useState([]); const [sort, setSort] = useState({}); const [search, setSearch] = useState(""); const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); const [isSearching, setIsSearching] = useState(false); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); @@ -46,9 +43,6 @@ const UptimeMonitors = () => { const isAdmin = useIsAdmin(); const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); const authState = useSelector((state) => state.auth); - const dispatch = useDispatch({}); - const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const fetchParams = useMemo( () => ({ diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 84c7ff971..457e1ad87 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -30,15 +30,6 @@ const CHECK_MODEL_LOOKUP = { hardware: HardwareCheck, }; -const MODEL_TYPE_MAPPING = { - http: "checks", - ping: "checks", - docker: "checks", - port: "checks", - pagespeed: "pagespeedchecks", - hardware: "hardwarechecks", -}; - /** * Get all monitors * @async diff --git a/Server/utils/dataUtils.js b/Server/utils/dataUtils.js index 8fbb22821..008578158 100644 --- a/Server/utils/dataUtils.js +++ b/Server/utils/dataUtils.js @@ -26,7 +26,6 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { const min = calculatePercentile(checks, 0); const max = calculatePercentile(checks, 95); const normalizedChecks = checks.map((check) => { - console.log(check); const originalResponseTime = check.responseTime; // Normalize the response time between 1 and 100 let normalizedResponseTime = From 35796dccd470a95648aa52cbad500787a87940f0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 10:06:50 -0800 Subject: [PATCH 47/64] add normalize check --- Server/db/mongo/modules/monitorModule.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 457e1ad87..189243295 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -689,10 +689,12 @@ const getMonitorsByTeamId = async (req, res) => { }, }, ]); - result = result.map((monitor) => { - monitor.checks = NormalizeData(monitor.checks, 10, 100); - return monitor; - }); + if (normalize) { + result = result.map((monitor) => { + monitor.checks = NormalizeData(monitor.checks, 10, 100); + return monitor; + }); + } return { monitors: result, monitorCount }; } catch (error) { From 2694f836c7c5d3c0971164c921ebccc608967f86 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 10:17:51 -0800 Subject: [PATCH 48/64] fix destructuring --- Server/db/mongo/modules/monitorModule.js | 1 - Server/utils/dataUtils.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 189243295..aa4cafbb7 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -695,7 +695,6 @@ const getMonitorsByTeamId = async (req, res) => { return monitor; }); } - return { monitors: result, monitorCount }; } catch (error) { error.service = SERVICE_NAME; diff --git a/Server/utils/dataUtils.js b/Server/utils/dataUtils.js index 008578158..ca6ca66df 100644 --- a/Server/utils/dataUtils.js +++ b/Server/utils/dataUtils.js @@ -47,7 +47,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { return normalizedChecks; } else { return checks.map((check) => { - return { ...check._doc, originalResponseTime: check.responseTime }; + return { ...check, originalResponseTime: check.responseTime }; }); } }; From 8d8082bb8303dcc06d78bba9132d617d93e6dd92 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 11:53:13 -0800 Subject: [PATCH 49/64] use redux state for pagination --- Client/src/Pages/Uptime/Home/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 7e1f056ca..3dffb3ac9 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -14,6 +14,7 @@ import { getUptimeSummaryByTeamId, getUptimeMonitorsByTeamId, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useSelector, useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; @@ -25,13 +26,12 @@ const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; const UptimeMonitors = () => { // Redux state - + const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage); // Local state const [monitors, setMonitors] = useState([]); const [sort, setSort] = useState({}); const [search, setSearch] = useState(""); const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); const [isSearching, setIsSearching] = useState(false); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); From cf5152d0ec6f95af55a0901724967d1fad074139 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 12:01:28 -0800 Subject: [PATCH 50/64] remove unused dispatch from pagespeed --- Client/src/Pages/PageSpeed/index.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index b18148aa8..483e2b0da 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -1,8 +1,7 @@ import { Box, Button, Grid, Stack } from "@mui/material"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; -import { useDispatch, useSelector } from "react-redux"; -import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; +import { useSelector } from "react-redux"; import Fallback from "../../Components/Fallback"; import "./index.css"; import { useNavigate } from "react-router"; @@ -15,16 +14,12 @@ import { Heading } from "../../Components/Heading"; import { useIsAdmin } from "../../Hooks/useIsAdmin"; const PageSpeed = () => { const theme = useTheme(); - const dispatch = useDispatch(); const navigate = useNavigate(); const isAdmin = useIsAdmin(); const { user, authToken } = useSelector((state) => state.auth); const [isLoading, setIsLoading] = useState(true); const [monitors, setMonitors] = useState([]); const [monitorCount, setMonitorCount] = useState(0); - useEffect(() => { - dispatch(getPageSpeedByTeamId(authToken)); - }, [authToken, dispatch]); useEffect(() => { const fetchMonitors = async () => { From 5da542b99e4c1d90050938df15c4d2a8b7771e35 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 12:01:43 -0800 Subject: [PATCH 51/64] formatting --- .../src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 6cd91ac28..b2da2cbe6 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -40,7 +40,7 @@ export const checkEndpointResolution = createAsyncThunk( const res = await networkService.checkEndpointResolution({ authToken: authToken, monitorURL: monitorURL, - }) + }); return res.data; } catch (error) { if (error.response && error.response.data) { @@ -53,7 +53,7 @@ export const checkEndpointResolution = createAsyncThunk( return thunkApi.rejectWithValue(payload); } } -) +); export const getPagespeedMonitorById = createAsyncThunk( "monitors/getMonitorById", @@ -88,7 +88,6 @@ export const getPageSpeedByTeamId = createAsyncThunk( teamId: user.teamId, types: ["pagespeed"], }); - return res.data; } catch (error) { if (error.response && error.response.data) { From 7cfc06b2dacaa0758ef35a39838b63dd5eee8bf1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:40:57 -0800 Subject: [PATCH 52/64] Remove summary route --- Server/routes/monitorRoute.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index f3af8e806..92b529417 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -17,6 +17,7 @@ class MonitorRoutes { "/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById ); + this.router.get( "/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById @@ -30,10 +31,7 @@ class MonitorRoutes { ); }); this.router.get("/:monitorId", this.monitorController.getMonitorById); - this.router.get( - "/team/summary/:teamId", - this.monitorController.getMonitorsSummaryByTeamId - ); + this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId); this.router.get( From 8f572ba01d32872470ebd1a73c3cfb2fb07ec22d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:42:08 -0800 Subject: [PATCH 53/64] Remove summary controller method, rewrite getMonitosByTeamId --- Server/controllers/monitorController.js | 97 ++++++------------------- 1 file changed, 22 insertions(+), 75 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index f5e7994b2..686661a00 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -1,13 +1,11 @@ import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByTeamIdValidation, + getMonitorsByTeamIdParamValidation, + getMonitorsByTeamIdQueryValidation, createMonitorBodyValidation, getMonitorURLByQueryValidation, editMonitorBodyValidation, - getMonitorsSummaryByTeamIdParamValidation, - getMonitorsSummaryByTeamIdQueryValidation, - getMonitorsByTeamIdQueryValidation, pauseMonitorParamValidation, getMonitorStatsByIdParamValidation, getMonitorStatsByIdQueryValidation, @@ -204,77 +202,6 @@ class MonitorController { } }; - /** - * Retrieves all monitors and a summary for a team based on the team ID. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.teamId - The ID of the team. - * @property {Object} req.query - The query parameters of the request. - * @property {string} req.query.type - The type of the request. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ - getMonitorsSummaryByTeamId = async (req, res, next) => { - try { - await getMonitorsSummaryByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsSummaryByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const { teamId } = req.params; - const { type } = req.query; - const monitorsSummary = await this.db.getMonitorsSummaryByTeamId(teamId, type); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), - data: monitorsSummary, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId")); - } - }; - - /** - * Retrieves all monitors associated with a team by the team's ID. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.params - The parameters of the request. - * @property {string} req.params.teamId - The ID of the team. - * @property {Object} req.query - The query parameters of the request. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message, and the data containing the monitors for the team. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ - getMonitorsByTeamId = async (req, res, next) => { - try { - await getMonitorsByTeamIdValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - } catch (error) { - next(handleValidationError(error, SERVICE_NAME)); - return; - } - - try { - const teamId = req.params.teamId; - const monitors = await this.db.getMonitorsByTeamId(req, res); - return res.status(200).json({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), - data: monitors, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); - next(error); - } - }; - /** * Creates a new monitor and adds it to the job queue. * @async @@ -564,6 +491,26 @@ class MonitorController { next(handleError(error, SERVICE_NAME, "addDemoMonitors")); } }; + + getMonitorsByTeamId = async (req, res, next) => { + try { + await getMonitorsByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + } catch (error) { + next(handleValidationError(error, SERVICE_NAME)); + } + + try { + const monitors = await this.db.getMonitorsByTeamId(req); + return res.status(200).json({ + success: true, + msg: "good", + data: monitors, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMonitorsForDisplay")); + } + }; } export default MonitorController; From 87f9c35c74e3b934589a1a56cb61f26461146d16 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:42:43 -0800 Subject: [PATCH 54/64] remove unused validation, update monitors by team ID validation --- Server/validation/joi.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 825c90cdb..d30235048 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -136,32 +136,12 @@ const getMonitorByIdQueryValidation = joi.object({ normalize: joi.boolean(), }); -const getMonitorsSummaryByTeamIdParamValidation = joi.object({ - teamId: joi.string().required(), -}); - -const getMonitorsSummaryByTeamIdQueryValidation = joi.object({ - type: joi - .alternatives() - .try( - joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port"), - joi - .array() - .items( - joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port") - ) - ), -}); - -const getMonitorsByTeamIdValidation = joi.object({ +const getMonitorsByTeamIdParamValidation = joi.object({ teamId: joi.string().required(), }); const getMonitorsByTeamIdQueryValidation = joi.object({ - status: joi.boolean(), - checkOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - normalize: joi.boolean(), type: joi .alternatives() .try( @@ -467,9 +447,7 @@ export { createMonitorBodyValidation, getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsSummaryByTeamIdParamValidation, - getMonitorsSummaryByTeamIdQueryValidation, - getMonitorsByTeamIdValidation, + getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, getMonitorStatsByIdParamValidation, getMonitorStatsByIdQueryValidation, From 979110dd7709493992ad1e4f713ed5523f91e818 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:44:24 -0800 Subject: [PATCH 55/64] Rewrite team monitors query --- Server/db/mongo/modules/monitorModule.js | 329 ++++++++++------------- 1 file changed, 144 insertions(+), 185 deletions(-) diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index aa4cafbb7..77c290c52 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -505,202 +505,162 @@ const getMonitorById = async (monitorId) => { } }; -/** - * Get monitors and Summary by TeamID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise>} - * @throws {Error} - */ +const getMonitorsByTeamId = async (req) => { + let { limit, type, page, rowsPerPage, filter, field, order } = req.query; -const getMonitorsSummaryByTeamId = async (teamId, type) => { - try { - const monitorCounts = await Monitor.aggregate([ - { - $match: { - type: { $in: type }, - }, - }, - { - $facet: { - total: [{ $count: "count" }], - up: [{ $match: { status: true } }, { $count: "count" }], - down: [{ $match: { status: false } }, { $count: "count" }], - paused: [{ $match: { isActive: false } }, { $count: "count" }], - }, - }, - { - $project: { - total: { $arrayElemAt: ["$total.count", 0] }, - up: { $arrayElemAt: ["$up.count", 0] }, - down: { $arrayElemAt: ["$down.count", 0] }, - paused: { $arrayElemAt: ["$paused.count", 0] }, - }, - }, - ]); - return { monitorCounts: monitorCounts[0] }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; - throw error; + // Parse ints + limit = parseInt(limit); + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); + + // Build the match stage + const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; } -}; -/** - * Get monitors by TeamID - * @async - * @param {Express.Request} req - * @param {Express.Response} res - * @returns {Promise>} - * @throws {Error} - */ -const getMonitorsByTeamId = async (req, res) => { - try { - let { - limit, - type, - status, - checkOrder, - normalize, - page, - rowsPerPage, - filter, - field, - order, - } = req.query; + const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const monitorQuery = { teamId: req.params.teamId }; - const monitorCount = await Monitor.countDocuments(monitorQuery); + const sort = { [field]: order === "asc" ? 1 : -1 }; - if (type !== undefined) { - monitorQuery.type = Array.isArray(type) ? { $in: type } : type; - } - // Add filter if provided - // $options: "i" makes the search case-insensitive - if (filter !== undefined) { - monitorQuery.$or = [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ]; - } + if (filter !== undefined) { + matchStage.$or = [ + { name: { $regex: filter, $options: "i" } }, + { url: { $regex: filter, $options: "i" } }, + ]; + } - // Pagination - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - - // Build Sort option - const sort = { [field]: order === "asc" ? 1 : -1 }; - - const matchStage = { teamId: new ObjectId(req.params.teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - if (filter !== undefined) { - matchStage.$or = [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ]; - } - - let result = await Monitor.aggregate([ - { $match: matchStage }, - { $skip: parseInt(skip) }, - ...(rowsPerPage ? [{ $limit: parseInt(rowsPerPage) }] : []), - { $sort: sort }, - { - $lookup: { - from: "checks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - ...(status && { status }), - }, - }, - { $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } }, - { $limit: parseInt(limit) || 0 }, - ], - as: "standardchecks", - }, - }, - { - $lookup: { - from: "pagespeedchecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - ...(status && { status }), - }, - }, - { $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } }, - { $limit: parseInt(limit) || 0 }, - ], - as: "pagespeedchecks", - }, - }, - { - $lookup: { - from: "hardwarechecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - ...(status && { status }), - }, - }, - { $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } }, - { $limit: parseInt(limit) || 0 }, - ], - as: "hardwarechecks", - }, - }, - { - $addFields: { - checks: { - $switch: { - branches: [ - { - case: { $in: ["$type", ["http", "ping", "docker", "port"]] }, - then: "$standardchecks", + const results = await Monitor.aggregate([ + { $match: matchStage }, + { + $facet: { + summary: [ + { + $group: { + _id: null, + totalMonitors: { $sum: 1 }, + upMonitors: { + $sum: { + $cond: [{ $eq: ["$status", true] }, 1, 0], }, - { - case: { $eq: ["$type", "pagespeed"] }, - then: "$pagespeedchecks", + }, + downMonitors: { + $sum: { + $cond: [{ $eq: ["$status", false] }, 1, 0], }, - { - case: { $eq: ["$type", "hardware"] }, - then: "$hardwarechecks", + }, + pausedMonitors: { + $sum: { + $cond: [{ $eq: ["$isActive", false] }, 1, 0], }, - ], - default: [], + }, }, }, - }, + { + $project: { + _id: 0, + }, + }, + ], + monitors: [ + { $sort: sort }, + { $skip: skip }, + ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []), + { + $lookup: { + from: "checks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "standardchecks", + }, + }, + { + $lookup: { + from: "pagespeedchecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "pagespeedchecks", + }, + }, + { + $lookup: { + from: "hardwarechecks", + let: { monitorId: "$_id" }, + pipeline: [ + { + $match: { + $expr: { $eq: ["$monitorId", "$$monitorId"] }, + }, + }, + { $sort: { createdAt: -1 } }, + ...(limit ? [{ $limit: limit }] : []), + ], + as: "hardwarechecks", + }, + }, + { + $addFields: { + checks: { + $switch: { + branches: [ + { + case: { $in: ["$type", ["http", "ping", "docker", "port"]] }, + then: "$standardchecks", + }, + { + case: { $eq: ["$type", "pagespeed"] }, + then: "$pagespeedchecks", + }, + { + case: { $eq: ["$type", "hardware"] }, + then: "$hardwarechecks", + }, + ], + default: [], + }, + }, + }, + }, + { + $project: { + standardchecks: 0, + pagespeedchecks: 0, + hardwarechecks: 0, + }, + }, + ], }, - { - $project: { - standardchecks: 0, - pagespeedchecks: 0, - hardwarechecks: 0, - }, + }, + { + $project: { + summary: { $arrayElemAt: ["$summary", 0] }, + monitors: 1, }, - ]); - if (normalize) { - result = result.map((monitor) => { - monitor.checks = NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - } - return { monitors: result, monitorCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByTeamId"; - throw error; - } + }, + ]); + + let { monitors, summary } = results[0]; + monitors = monitors.map((monitor) => { + monitor.checks = NormalizeData(monitor.checks, 10, 100); + return monitor; + }); + return { monitors, summary }; }; /** @@ -832,9 +792,8 @@ export { getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, - getUptimeDetailsById, - getMonitorsSummaryByTeamId, getMonitorsByTeamId, + getUptimeDetailsById, createMonitor, deleteMonitor, deleteAllMonitors, From 1805ca8d172ae114b87be2ee5f6164d9da3df970 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:45:34 -0800 Subject: [PATCH 56/64] Remove unnecessary params from getMonitorsByTeamId --- Client/src/Utils/NetworkService.js | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index cc70b0f69..58f3fcb5a 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -157,9 +157,6 @@ class NetworkService { * @param {string} config.teamId - The ID of the team whose monitors are to be retrieved. * @param {number} [config.limit] - The maximum number of checks to retrieve. 0 for all, -1 for none * @param {Array} [config.types] - The types of monitors to retrieve. - * @param {string} [config.status] - The status of the monitors to retrieve. - * @param {string} [config.checkOrder] - The order in which to sort the retrieved monitors. - * @param {boolean} [config.normalize] - Whether to normalize the retrieved monitors. * @param {number} [config.page] - The page number for pagination. * @param {number} [config.rowsPerPage] - The number of rows per page for pagination. * @param {string} [config.filter] - The filter to apply to the monitors. @@ -167,21 +164,10 @@ class NetworkService { * @param {string} [config.order] - The order in which to sort the field. * @returns {Promise} The response from the axios GET request. */ + async getMonitorsByTeamId(config) { - const { - authToken, - teamId, - limit, - types, - status, - checkOrder, - normalize, - page, - rowsPerPage, - filter, - field, - order, - } = config; + const { authToken, teamId, limit, types, page, rowsPerPage, filter, field, order } = + config; const params = new URLSearchParams(); @@ -191,9 +177,6 @@ class NetworkService { params.append("type", type); }); } - if (status) params.append("status", status); - if (checkOrder) params.append("checkOrder", checkOrder); - if (normalize) params.append("normalize", normalize); if (page) params.append("page", page); if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); if (filter) params.append("filter", filter); From e616df276630230e1a366e344575d4f303da25ba Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:46:19 -0800 Subject: [PATCH 57/64] Remove unused thunks from uptime slice --- .../UptimeMonitors/uptimeMonitorsSlice.js | 92 ------------------- 1 file changed, 92 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 8782948ae..7ea34b9ea 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -78,62 +78,6 @@ export const getUptimeMonitorById = createAsyncThunk( } ); -export const getUptimeSummaryByTeamId = createAsyncThunk( - "monitors/getSummaryByTeamId", - async (token, thunkApi) => { - const user = jwtDecode(token); - try { - const res = await networkService.getMonitorsSummaryByTeamId({ - authToken: token, - teamId: user.teamId, - types: ["http", "ping", "docker", "port"], - }); - 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 getUptimeMonitorsByTeamId = createAsyncThunk( - "monitors/getMonitorsByTeamId", - async (config, thunkApi) => { - try { - const res = await networkService.getMonitorsByTeamId({ - authToken: config.authToken, - teamId: config.teamId, - limit: 25, - types: ["http", "ping", "docker", "port"], - status: null, - checkOrder: "desc", - normalize: true, - page: config.page, - rowsPerPage: config.rowsPerPage, - filter: config.filter, - field: config.sort.field, - order: config.sort.order, - }); - 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 updateUptimeMonitor = createAsyncThunk( "monitors/updateMonitor", async (data, thunkApi) => { @@ -289,42 +233,6 @@ const uptimeMonitorsSlice = createSlice({ }, extraReducers: (builder) => { builder - // ***************************************************** - // Summary by teamId - // ***************************************************** - - .addCase(getUptimeSummaryByTeamId.pending, (state) => { - state.isLoading = true; - }) - .addCase(getUptimeSummaryByTeamId.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.msg; - state.monitorsSummary = action.payload.data; - }) - .addCase(getUptimeSummaryByTeamId.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload ? action.payload.msg : "Getting uptime summary failed"; - }) - - // ***************************************************** - // Monitors by teamId - // ***************************************************** - .addCase(getUptimeMonitorsByTeamId.pending, (state) => { - state.isLoading = true; - }) - .addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Getting uptime monitors failed"; - }) // ***************************************************** // Create Monitor // ***************************************************** From bde4269fd163c12bdd2cd7bbe097f6e92728ddda Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 13:46:49 -0800 Subject: [PATCH 58/64] use new query in infra page --- Client/src/Pages/Infrastructure/index.jsx | 29 ++++++++++------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index be7d7e9ed..d9ef7528e 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { /* useDispatch, */ useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; @@ -50,12 +50,13 @@ function Infrastructure() { setRowsPerPage(parseInt(event.target.value)); setPage(0); }; - const [monitorState, setMonitorState] = useState({ monitors: [], total: 0 }); + const [monitors, setMonitors] = useState([]); + const [summary, setSummary] = useState({}); const { authToken } = useSelector((state) => state.auth); const user = jwtDecode(authToken); - const fetchMonitors = async () => { + const fetchMonitors = useCallback(async () => { try { setIsLoading(true); const response = await networkService.getMonitorsByTeamId({ @@ -63,29 +64,23 @@ function Infrastructure() { teamId: user.teamId, limit: 1, types: ["hardware"], - status: null, - checkOrder: "desc", - normalize: true, page: page, rowsPerPage: rowsPerPage, }); - setMonitorState({ - monitors: response?.data?.data?.monitors ?? [], - total: response?.data?.data?.monitorCount ?? 0, - }); + setMonitors(response?.data?.data?.monitors ?? []); + setSummary(response?.data?.data?.summary ?? {}); } catch (error) { console.error(error); } finally { setIsLoading(false); } - }; + }, [page, rowsPerPage, authToken, user.teamId]); useEffect(() => { fetchMonitors(); - }, [page, rowsPerPage]); + }, [fetchMonitors]); const { determineState } = useUtils(); - const { monitors, total: totalMonitors } = monitorState; // do it here function openDetails(id) { navigate(`/infrastructure/${id}`); @@ -191,7 +186,7 @@ function Infrastructure() { }; }); - let isActuallyLoading = isLoading && monitorState.monitors?.length === 0; + let isActuallyLoading = isLoading && monitors?.length === 0; return ( {isActuallyLoading ? ( - ) : monitorState.monitors?.length !== 0 ? ( + ) : monitors?.length !== 0 ? ( @@ -254,7 +249,7 @@ function Infrastructure() { borderColor={theme.palette.border.light} backgroundColor={theme.palette.background.accent} > - {totalMonitors} + {summary?.totalMonitors ?? 0} @@ -272,7 +267,7 @@ function Infrastructure() { data={monitorsAsRows} /> Date: Wed, 8 Jan 2025 13:47:20 -0800 Subject: [PATCH 59/64] use new query in PageSpeed page --- Client/src/Pages/PageSpeed/card.jsx | 2 +- Client/src/Pages/PageSpeed/index.jsx | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Client/src/Pages/PageSpeed/card.jsx b/Client/src/Pages/PageSpeed/card.jsx index a08a4fcd1..d5cb4afed 100644 --- a/Client/src/Pages/PageSpeed/card.jsx +++ b/Client/src/Pages/PageSpeed/card.jsx @@ -267,7 +267,7 @@ const Card = ({ monitor }) => { sx={{ gridColumnStart: 1, gridColumnEnd: 4 }} > diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 483e2b0da..98761be6f 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -19,7 +19,7 @@ const PageSpeed = () => { const { user, authToken } = useSelector((state) => state.auth); const [isLoading, setIsLoading] = useState(true); const [monitors, setMonitors] = useState([]); - const [monitorCount, setMonitorCount] = useState(0); + const [summary, setSummary] = useState({}); useEffect(() => { const fetchMonitors = async () => { @@ -30,9 +30,6 @@ const PageSpeed = () => { teamId: user.teamId, limit: 10, types: ["pagespeed"], - status: null, - checkOrder: "desc", - normalize: true, page: null, rowsPerPage: null, filter: null, @@ -41,7 +38,7 @@ const PageSpeed = () => { }); if (res?.data?.data?.monitors) { setMonitors(res.data.data.monitors); - setMonitorCount(res.data.data.monitorCount); + setSummary(res.data.data.summary); } } catch (error) { console.log(error); @@ -113,7 +110,7 @@ const PageSpeed = () => { borderColor={theme.palette.border.light} backgroundColor={theme.palette.background.accent} > - {monitorCount} + {summary?.totalMonitors ?? 0}
Date: Wed, 8 Jan 2025 13:47:42 -0800 Subject: [PATCH 60/64] Use new query in Uptime page --- .../Uptime/Home/UptimeDataTable/index.jsx | 57 +++++++++++++++++-- Client/src/Pages/Uptime/Home/actionsMenu.jsx | 12 +++- Client/src/Pages/Uptime/Home/index.jsx | 56 ++++++++++-------- 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx index d174c535f..faca0c612 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -12,13 +12,42 @@ import ActionsMenu from "../actionsMenu"; // Utils import { useTheme } from "@emotion/react"; -import useDebounce from "../../../../Utils/debounce"; -import { useState } from "react"; import useUtils from "../../utils"; import { memo } from "react"; import { useNavigate } from "react-router-dom"; import "../index.css"; +import PropTypes from "prop-types"; +/** + * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities + * @param {Object} props - Component props + * @param {boolean} props.isAdmin - Whether the current user has admin privileges + * @param {boolean} props.isLoading - Loading state of the table + * @param {Array<{ + * _id: string, + * url: string, + * title: string, + * percentage: number, + * percentageColor: string, + * monitor: { + * _id: string, + * type: string, + * checks: Array + * } + * }>} props.monitors - Array of monitor objects to display + * @param {number} props.monitorCount - Total count of monitors + * @param {Object} props.sort - Current sort configuration + * @param {string} props.sort.field - Field to sort by + * @param {'asc'|'desc'} props.sort.order - Sort direction + * @param {Function} props.setSort - Callback to update sort configuration + * @param {string} props.search - Current search query + * @param {Function} props.setSearch - Callback to update search query + * @param {boolean} props.isSearching - Whether a search is in progress + * @param {Function} props.setIsSearching - Callback to update search state + * @param {Function} props.setIsLoading - Callback to update loading state + * @param {Function} props.triggerUpdate - Callback to trigger a data refresh + * @returns {JSX.Element} Rendered component + */ const UptimeDataTable = ({ isAdmin, isLoading, @@ -30,6 +59,7 @@ const UptimeDataTable = ({ setSearch, isSearching, setIsSearching, + setIsLoading, triggerUpdate, }) => { const { determineState } = useUtils(); @@ -37,8 +67,6 @@ const UptimeDataTable = ({ const theme = useTheme(); const navigate = useNavigate(); - //Utils - const debouncedFilter = useDebounce(search, 500); const handleSearch = (value) => { setIsSearching(true); setSearch(value); @@ -146,6 +174,7 @@ const UptimeDataTable = ({ monitor={row.monitor} isAdmin={isAdmin} updateRowCallback={triggerUpdate} + setIsLoading={setIsLoading} pauseCallback={triggerUpdate} /> ), @@ -187,7 +216,7 @@ const UptimeDataTable = ({
- {isSearching && ( + {(isSearching || isLoading) && ( <> { +const ActionsMenu = ({ + monitor, + isAdmin, + updateRowCallback, + pauseCallback, + setIsLoading, +}) => { const [anchorEl, setAnchorEl] = useState(null); const [actions, setActions] = useState({}); const [isOpen, setIsOpen] = useState(false); @@ -33,7 +38,6 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) => ); if (action.meta.requestStatus === "fulfilled") { setIsOpen(false); // close modal - dispatch(getUptimeMonitorsByTeamId(authState.authToken)); updateRowCallback(); createToast({ body: "Monitor deleted successfully." }); } else { @@ -43,6 +47,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) => const handlePause = async () => { try { + setIsLoading(true); const action = await dispatch( pauseUptimeMonitor({ authToken, monitorId: monitor._id }) ); @@ -223,6 +228,7 @@ ActionsMenu.propTypes = { isAdmin: PropTypes.bool, updateRowCallback: PropTypes.func, pauseCallback: PropTypes.func, + setIsLoading: PropTypes.func, }; export default ActionsMenu; diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 3dffb3ac9..391be0aed 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -9,11 +9,7 @@ import { Pagination } from "../../../Components/Table/TablePagination"; // Utils import { useTheme } from "@emotion/react"; -import { useEffect, useState, useCallback, useMemo, useRef } from "react"; -import { - getUptimeSummaryByTeamId, - getUptimeMonitorsByTeamId, -} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { useEffect, useState, useCallback, useMemo } from "react"; import { setRowsPerPage } from "../../../Features/UI/uiSlice"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useSelector, useDispatch } from "react-redux"; @@ -21,6 +17,7 @@ import { useNavigate } from "react-router-dom"; import { createToast } from "../../../Utils/toastUtils"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import useDebounce from "../../../Utils/debounce"; +import { networkService } from "../../../main"; const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; @@ -33,7 +30,9 @@ const UptimeMonitors = () => { const [search, setSearch] = useState(""); const [page, setPage] = useState(0); const [isSearching, setIsSearching] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); + const [monitorsSummary, setMonitorsSummary] = useState({}); // Utils const debouncedFilter = useDebounce(search, 500); @@ -41,7 +40,6 @@ const UptimeMonitors = () => { const theme = useTheme(); const navigate = useNavigate(); const isAdmin = useIsAdmin(); - const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors); const authState = useSelector((state) => state.auth); const fetchParams = useMemo( @@ -89,30 +87,38 @@ const UptimeMonitors = () => { const fetchMonitors = useCallback(async () => { try { - const action = await dispatch(getUptimeMonitorsByTeamId(fetchParams)); - if (action.payload.success) { - const { monitors } = action.payload.data; - const mappedMonitors = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setMonitors(mappedMonitors); - } else { - // TODO: Check for other errors? - throw new Error("Error fetching monitors"); - } + setIsLoading(true); + const config = fetchParams; + const res = await networkService.getMonitorsByTeamId({ + authToken: config.authToken, + teamId: config.teamId, + limit: 25, + types: ["http", "ping", "docker", "port"], + page: config.page, + rowsPerPage: config.rowsPerPage, + filter: config.filter, + field: config.sort.field, + order: config.sort.order, + }); + const { monitors, summary } = res.data.data; + const mappedMonitors = monitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(mappedMonitors); + setMonitorsSummary(summary); } catch (error) { createToast({ body: "Error fetching monitors", }); } finally { + setIsLoading(false); setIsSearching(false); } - }, [fetchParams, dispatch, getMonitorWithPercentage, theme]); + }, [fetchParams, getMonitorWithPercentage, theme]); useEffect(() => { - dispatch(getUptimeSummaryByTeamId(authState.authToken)); fetchMonitors(); - }, [fetchMonitors, monitorUpdateTrigger, authState.authToken, dispatch]); + }, [fetchMonitors, monitorUpdateTrigger]); const handleChangePage = (event, newPage) => { setPage(newPage); @@ -131,7 +137,7 @@ const UptimeMonitors = () => { const triggerUpdate = () => { setMonitorUpdateTrigger((prev) => !prev); }; - const totalMonitors = monitorsSummary?.monitorCounts?.total; + const totalMonitors = monitorsSummary.totalMonitors; const hasMonitors = totalMonitors > 0; const canAddMonitor = isAdmin && hasMonitors; @@ -176,19 +182,20 @@ const UptimeMonitors = () => { > { setSearch={setSearch} isSearching={isSearching} setIsSearching={setIsSearching} + setIsLoading={setIsLoading} triggerUpdate={triggerUpdate} /> Date: Wed, 8 Jan 2025 14:03:23 -0800 Subject: [PATCH 61/64] readd skeleton --- Client/src/Pages/Uptime/Home/index.jsx | 84 ++++++++++++++------------ 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 391be0aed..4ccacd81e 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -173,48 +173,52 @@ const UptimeMonitors = () => { { <> {!isLoading && !hasMonitors && } - {hasMonitors && ( - <> - - + ) : ( + hasMonitors && ( + <> + + + + + + - - - - - - + + ) )} } From 40fbe4a8229cc0c286a3418d7d5e2eec9efada91 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 14:17:29 -0800 Subject: [PATCH 62/64] move filtering to the monitors facet --- Client/src/Pages/Uptime/Home/index.jsx | 4 ++-- Server/db/mongo/modules/monitorModule.js | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 4ccacd81e..449c7dee3 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -137,8 +137,8 @@ const UptimeMonitors = () => { const triggerUpdate = () => { setMonitorUpdateTrigger((prev) => !prev); }; - const totalMonitors = monitorsSummary.totalMonitors; - const hasMonitors = totalMonitors > 0; + const totalMonitors = monitorsSummary?.totalMonitors ?? 0; + const hasMonitors = monitorsSummary?.totalMonitors ?? 0; const canAddMonitor = isAdmin && hasMonitors; return ( diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 77c290c52..7b554ff32 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -523,13 +523,6 @@ const getMonitorsByTeamId = async (req) => { const sort = { [field]: order === "asc" ? 1 : -1 }; - if (filter !== undefined) { - matchStage.$or = [ - { name: { $regex: filter, $options: "i" } }, - { url: { $regex: filter, $options: "i" } }, - ]; - } - const results = await Monitor.aggregate([ { $match: matchStage }, { @@ -563,6 +556,18 @@ const getMonitorsByTeamId = async (req) => { }, ], monitors: [ + ...(filter !== undefined + ? [ + { + $match: { + $or: [ + { name: { $regex: filter, $options: "i" } }, + { url: { $regex: filter, $options: "i" } }, + ], + }, + }, + ] + : []), { $sort: sort }, { $skip: skip }, ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []), From de01a430a3f23c1627e2ff75fa5223aa375b893e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 15:13:35 -0800 Subject: [PATCH 63/64] fix unnecessary rerenders --- Client/src/Components/Inputs/Search/index.jsx | 1 + .../Uptime/Home/UptimeDataTable/index.jsx | 67 +++++++++++++------ Client/src/Pages/Uptime/Home/index.jsx | 6 +- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index 9a1995726..2491020f0 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -132,6 +132,7 @@ const Search = ({ } return filtered; }} + getOptionKey={(option) => option.id} renderOption={(props, option) => { const { key, ...optionProps } = props; return ( diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx index faca0c612..d79f4e270 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -13,10 +13,47 @@ import ActionsMenu from "../actionsMenu"; // Utils import { useTheme } from "@emotion/react"; import useUtils from "../../utils"; -import { memo } from "react"; +import { useState, memo, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import "../index.css"; import PropTypes from "prop-types"; +import { useTraceUpdate } from "../../../../Hooks/useTraceUpdate"; + +const SearchComponent = memo( + ({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => { + const [localSearch, setLocalSearch] = useState(debouncedSearch); + const handleSearch = useCallback( + (value) => { + setIsSearching(true); + setLocalSearch(value); + onSearchChange(value); + }, + [onSearchChange, setIsSearching] + ); + + return ( + + + + ); + } +); +SearchComponent.displayName = "SearchComponent"; +SearchComponent.propTypes = { + monitors: PropTypes.array, + debouncedSearch: PropTypes.string, + onSearchChange: PropTypes.func, + setIsSearching: PropTypes.func, +}; /** * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities @@ -55,7 +92,7 @@ const UptimeDataTable = ({ monitorCount, sort, setSort, - search, + debouncedSearch, setSearch, isSearching, setIsSearching, @@ -67,11 +104,6 @@ const UptimeDataTable = ({ const theme = useTheme(); const navigate = useNavigate(); - const handleSearch = (value) => { - setIsSearching(true); - setSearch(value); - }; - const handleSort = (field) => { let order = ""; if (sort.field !== field) { @@ -202,18 +234,13 @@ const UptimeDataTable = ({ > {monitorCount} - - - + + {(isSearching || isLoading) && ( @@ -280,7 +307,7 @@ UptimeDataTable.propTypes = { order: PropTypes.oneOf(["asc", "desc"]), }), setSort: PropTypes.func, - search: PropTypes.string, + debouncedSearch: PropTypes.string, setSearch: PropTypes.func, isSearching: PropTypes.bool, setIsSearching: PropTypes.func, diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 449c7dee3..4c5656b2c 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -134,9 +134,9 @@ const UptimeMonitors = () => { setPage(0); }; - const triggerUpdate = () => { + const triggerUpdate = useCallback(() => { setMonitorUpdateTrigger((prev) => !prev); - }; + }, []); const totalMonitors = monitorsSummary?.totalMonitors ?? 0; const hasMonitors = monitorsSummary?.totalMonitors ?? 0; const canAddMonitor = isAdmin && hasMonitors; @@ -203,7 +203,7 @@ const UptimeMonitors = () => { monitorCount={totalMonitors} sort={sort} setSort={setSort} - search={search} + debouncedSearch={debouncedFilter} setSearch={setSearch} isSearching={isSearching} setIsSearching={setIsSearching} From 6d82cb7eb2087b6d5ac0d1aff6e634df2de2e0b2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jan 2025 15:22:06 -0800 Subject: [PATCH 64/64] remove missing import --- Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx index d79f4e270..31b6182fc 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -17,7 +17,6 @@ import { useState, memo, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import "../index.css"; import PropTypes from "prop-types"; -import { useTraceUpdate } from "../../../../Hooks/useTraceUpdate"; const SearchComponent = memo( ({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => {