From 93d96b99a4fbffbffb4deab5f6e1ba37e71fcc71 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 11:40:34 +0800 Subject: [PATCH 01/26] Add infrastrucutre/details route --- Client/src/App.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 151512701..03c406a6c 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -39,6 +39,7 @@ import { getAppSettings } from "./Features/Settings/settingsSlice"; import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Infrastructure } from "./Pages/Infrastructure"; +import InfrastructureDetails from "./Pages/Infrastructure/details"; function App() { const AdminCheckedRegister = withAdminCheck(Register); const MonitorsWithAdminProp = withAdminProp(Monitors); @@ -48,6 +49,7 @@ function App() { const MaintenanceWithAdminProp = withAdminProp(Maintenance); const SettingsWithAdminProp = withAdminProp(Settings); const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings); + const InfrastructureDetailsWithAdminProp = withAdminProp(InfrastructureDetails); const mode = useSelector((state) => state.ui.mode); const { authToken } = useSelector((state) => state.auth); const dispatch = useDispatch(); @@ -128,6 +130,11 @@ function App() { element={} /> + } + /> + } From 08323202b711869fdaf5aa09572143c9706b8dec Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 11:40:45 +0800 Subject: [PATCH 02/26] Create stat boxes --- .../src/Pages/Infrastructure/details/data.js | 128 ++++++++++++++++++ .../Pages/Infrastructure/details/index.jsx | 99 ++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 Client/src/Pages/Infrastructure/details/data.js create mode 100644 Client/src/Pages/Infrastructure/details/index.jsx diff --git a/Client/src/Pages/Infrastructure/details/data.js b/Client/src/Pages/Infrastructure/details/data.js new file mode 100644 index 000000000..029de5203 --- /dev/null +++ b/Client/src/Pages/Infrastructure/details/data.js @@ -0,0 +1,128 @@ +function generateMonitorEntries(monitorId, count = 10, options = {}) { + const defaultOptions = { + timeRange: { + start: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago + end: new Date(), + }, + statusVariation: [true, false], + responseTimeRange: [50, 500], + cpuUsageRange: [0, 100], + memoryUsageRange: [0, 100], + diskUsageRange: [0, 100], + }; + + const mergedOptions = { ...defaultOptions, ...options }; + + return Array.from({ length: count }, (_, index) => { + const createdAt = new Date( + mergedOptions.timeRange.start.getTime() + + (index * + (mergedOptions.timeRange.end.getTime() - + mergedOptions.timeRange.start.getTime())) / + count + ); + + return { + _id: "123", + monitorId: monitorId, + status: randomFromArray(mergedOptions.statusVariation), + responseTime: randomInRange(mergedOptions.responseTimeRange), + statusCode: randomStatusCode(), + message: randomMessage(), + cpu: { + physical_core: randomInRange([4, 8]), + logical_core: randomInRange([4, 16]), + frequency: randomInRange([10, 4000]), + temperature: randomInRange([20, 90]), + free_percent: 100 - randomInRange(mergedOptions.cpuUsageRange), + usage_percent: randomInRange(mergedOptions.cpuUsageRange), + _id: "123", + }, + memory: { + total_bytes: randomInRange([8, 32]) * 1024 * 1024 * 1024, + available_bytes: randomInRange([4, 16]) * 1024 * 1024 * 1024, + used_bytes: randomInRange([4, 16]) * 1024 * 1024 * 1024, + usage_percent: randomInRange(mergedOptions.memoryUsageRange), + _id: "123", + }, + disk: [ + { + read_speed_bytes: randomInRange([100, 1000]) * 1024 * 1024, + write_speed_bytes: randomInRange([100, 1000]) * 1024 * 1024, + total_bytes: randomInRange([100, 1000]) * 1024 * 1024 * 1024, + free_bytes: randomInRange([50, 500]) * 1024 * 1024 * 1024, + usage_percent: randomInRange(mergedOptions.diskUsageRange), + _id: "123", + }, + ], + host: { + os: randomOS(), + platform: randomPlatform(), + kernel_version: randomKernelVersion(), + _id: "123", + }, + errors: randomErrors(), + expiry: new Date(createdAt.getTime() + 365 * 24 * 60 * 60 * 1000), + createdAt: createdAt, + updatedAt: createdAt, + __v: 0, + }; + }); +} + +// Helper functions +function randomInRange([min, max]) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function randomFromArray(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +function randomStatusCode() { + const statusCodes = [200, 201, 204, 400, 401, 403, 404, 500, 502, 503]; + return randomFromArray(statusCodes); +} + +function randomMessage() { + const messages = [ + "OK", + "Created", + "No Content", + "Bad Request", + "Unauthorized", + "Forbidden", + "Not Found", + "Internal Server Error", + ]; + return randomFromArray(messages); +} + +function randomOS() { + const oss = ["Windows", "Linux", "macOS", "Ubuntu", "CentOS"]; + return randomFromArray(oss); +} + +function randomPlatform() { + const platforms = ["x64", "x86", "ARM", "ARM64"]; + return randomFromArray(platforms); +} + +function randomKernelVersion() { + return `${randomInRange([4, 6])}.${randomInRange([0, 20])}.${randomInRange([0, 100])}`; +} + +function randomErrors() { + const possibleErrors = [ + "Network timeout", + "Connection refused", + "SSL certificate error", + "DNS resolution failed", + "", + ]; + return Math.random() < 0.2 ? [randomFromArray(possibleErrors)] : []; +} + +// Usage +const monitorId = "123"; +export const monitorData = generateMonitorEntries(monitorId, 20); diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx new file mode 100644 index 000000000..ec5c2d066 --- /dev/null +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -0,0 +1,99 @@ +import { monitorData } from "./data"; +import { useParams } from "react-router-dom"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import { Stack, Box, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const bytesToGB = (bytes) => { + if (typeof bytes !== "number") return 0; + if (bytes === 0) return 0; + const GB = bytes / (1024 * 1024 * 1024); + return Number(GB.toFixed(0)); +}; + +const BaseBox = ({ children }) => { + const theme = useTheme(); + return ( + + {children} + + ); +}; + +const StatBox = ({ heading, subHeading }) => { + return ( + + {heading} + {subHeading} + + ); +}; + +const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { + const theme = useTheme(); + return ( + +
+
+ ); +}; + +const InfrastructureDetails = () => { + const theme = useTheme(); + const { monitorId } = useParams(); + const testData = monitorData; + const latestCheck = testData[testData.length - 1]; + const navList = [ + { name: "infrastructure monitors", path: "/infrastructure" }, + { name: "details", path: `/infrastructure/${monitorId}` }, + ]; + + return ( + + + + + + + + + + + + + ); +}; + +export default InfrastructureDetails; From fbdd3c152055a61424ce87f49a2ac12fa1033da9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 11:42:42 +0800 Subject: [PATCH 03/26] Add custom gauge component --- .../Components/Charts/CustomGauge/index.css | 0 .../Components/Charts/CustomGauge/index.jsx | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 Client/src/Components/Charts/CustomGauge/index.css create mode 100644 Client/src/Components/Charts/CustomGauge/index.jsx diff --git a/Client/src/Components/Charts/CustomGauge/index.css b/Client/src/Components/Charts/CustomGauge/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/Charts/CustomGauge/index.jsx b/Client/src/Components/Charts/CustomGauge/index.jsx new file mode 100644 index 000000000..d00ab7f5f --- /dev/null +++ b/Client/src/Components/Charts/CustomGauge/index.jsx @@ -0,0 +1,108 @@ +import { useTheme } from "@emotion/react"; +import { useEffect, useState } from "react"; +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +import "./index.css"; + +/** + * A Performant SVG based circular gauge + * + * @component + * @param {Object} props - Component properties + * @param {number} [props.progress=0] - Progress percentage (0-100) + * @param {number} [props.radius=60] - Radius of the gauge circle + * @param {string} [props.color="#000000"] - Color of the progress stroke + * @param {number} [props.strokeWidth=15] - Width of the gauge stroke + * + * @example + * + * + * @returns {React.ReactElement} Rendered CustomGauge component + */ +const CustomGauge = ({ + progress = 0, + radius = 60, + color = "#000000", + strokeWidth = 15, +}) => { + // Calculate the length of the stroke for the circle + const circumference = 2 * Math.PI * radius; + const totalSize = radius * 2 + strokeWidth * 2; // This is the total size of the SVG, needed for the viewBox + const strokeLength = (progress / 100) * circumference; + const [offset, setOffset] = useState(circumference); + const theme = useTheme(); + + // Handle initial animation + useEffect(() => { + setOffset(circumference); + const timer = setTimeout(() => { + setOffset(circumference - strokeLength); + }, 100); + + return () => clearTimeout(timer); + }, [progress, circumference, strokeLength]); + + return ( + + + + + + + + {`${progress}%`} + + + ); +}; + +export default CustomGauge; + +CustomGauge.propTypes = { + progress: PropTypes.number, + radius: PropTypes.number, + color: PropTypes.string, + strokeWidth: PropTypes.number, +}; From d2f42f432fbbb084d12f4c6a6afc59ed8c2e0dd5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 11:43:29 +0800 Subject: [PATCH 04/26] Add styles for custom gauage --- Client/src/Components/Charts/CustomGauge/index.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Client/src/Components/Charts/CustomGauge/index.css b/Client/src/Components/Charts/CustomGauge/index.css index e69de29bb..1b64a6b9a 100644 --- a/Client/src/Components/Charts/CustomGauge/index.css +++ b/Client/src/Components/Charts/CustomGauge/index.css @@ -0,0 +1,14 @@ +.radial-chart { + position: relative; + display: inline-block; +} + +.radial-chart-base { + opacity: 0.3; +} + +.radial-chart-progress { + transform: rotate(-90deg); + transform-origin: center; + transition: stroke-dashoffset 1.5s ease-in-out; +} From ca9ca4f59acf095522f241f64c5b04b0766560f9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 12:02:37 +0800 Subject: [PATCH 05/26] Add gauges --- .../Pages/Infrastructure/details/index.jsx | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx index ec5c2d066..9ff3c57f1 100644 --- a/Client/src/Pages/Infrastructure/details/index.jsx +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -3,6 +3,7 @@ import { useParams } from "react-router-dom"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; +import CustomGauge from "../../../Components/Charts/CustomGauge"; const bytesToGB = (bytes) => { if (typeof bytes !== "number") return 0; @@ -43,7 +44,41 @@ const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) const theme = useTheme(); return ( -
+ + + {heading} + + + {metricOne} + {valueOne} + + + {metricTwo} + {valueTwo} + + +
); }; @@ -91,6 +126,40 @@ const InfrastructureDetails = () => { subHeading={"Active"} /> + + + + {latestCheck.disk.map((disk, idx) => { + return ( + + ); + })} + ); From 55e13d7deb30cfcbd3429ee0c37fd55d1fc6e92e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 12:04:47 +0800 Subject: [PATCH 06/26] Add area chart file --- Client/src/Components/Charts/AreaChart/index.jsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Client/src/Components/Charts/AreaChart/index.jsx diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx new file mode 100644 index 000000000..e69de29bb From 49d81cf0ca60d5eb8404aaca5dd056e3bf711673 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 12:05:26 +0800 Subject: [PATCH 07/26] Add area chart --- .../src/Components/Charts/AreaChart/index.jsx | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx index e69de29bb..ce4da9830 100644 --- a/Client/src/Components/Charts/AreaChart/index.jsx +++ b/Client/src/Components/Charts/AreaChart/index.jsx @@ -0,0 +1,144 @@ +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { createGradient } from "../Utils/gradientUtils"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * CustomAreaChart component for rendering an area chart with optional gradient and custom ticks. + * + * @param {Object} props - The properties object. + * @param {Array} props.data - The data array for the chart. + * @param {string} props.xKey - The key for the x-axis data. + * @param {string} props.yKey - The key for the y-axis data. + * @param {Object} [props.xTick] - Custom tick component for the x-axis. + * @param {Object} [props.yTick] - Custom tick component for the y-axis. + * @param {string} [props.strokeColor] - The stroke color for the area. + * @param {string} [props.fillColor] - The fill color for the area. + * @param {boolean} [props.gradient=false] - Whether to apply a gradient fill. + * @param {string} [props.gradientDirection="vertical"] - The direction of the gradient. + * @param {string} [props.gradientStartColor] - The start color of the gradient. + * @param {string} [props.gradientEndColor] - The end color of the gradient. + * @param {Object} [props.customTooltip] - Custom tooltip component. + * @returns {JSX.Element} The rendered area chart component. + * + * @example + * // Example usage of CustomAreaChart + * import React from 'react'; + * import CustomAreaChart from './CustomAreaChart'; + * import { TzTick, PercentTick, InfrastructureTooltip } from './chartUtils'; + * + * const data = [ + * { createdAt: '2023-01-01T00:00:00Z', cpu: { usage_percent: 0.5 } }, + * { createdAt: '2023-01-01T01:00:00Z', cpu: { usage_percent: 0.6 } }, + * // more data points... + * ]; + * + * const MyChartComponent = () => { + * return ( + * + * ); + * }; + * + * export default MyChartComponent; + */ +const CustomAreaChart = ({ + data, + dataKey, + xKey, + yKey, + xTick, + yTick, + strokeColor, + fillColor, + gradient = false, + gradientDirection = "vertical", + gradientStartColor, + gradientEndColor, + customTooltip, +}) => { + const theme = useTheme(); + return ( + + + + + {gradient === true && + createGradient({ + startColor: gradientStartColor, + endColor: gradientEndColor, + direction: gradientDirection, + })} + + + + {customTooltip ? ( + + ) : ( + + )} + + + ); +}; + +CustomAreaChart.propTypes = { + data: PropTypes.array.isRequired, + dataKey: PropTypes.string.isRequired, + xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself + yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself + xKey: PropTypes.string.isRequired, + yKey: PropTypes.string.isRequired, + fillColor: PropTypes.string, + strokeColor: PropTypes.string, + gradient: PropTypes.bool, + gradientDirection: PropTypes.string, + gradientStartColor: PropTypes.string, + gradientEndColor: PropTypes.string, + customTooltip: PropTypes.object, +}; + +export default CustomAreaChart; From 92ba0d84dbb72eae3ab7b939793e67d8dfc04d9f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 12:06:21 +0800 Subject: [PATCH 08/26] Add data for area chart --- .../src/Components/Charts/AreaChart/data.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Client/src/Components/Charts/AreaChart/data.js diff --git a/Client/src/Components/Charts/AreaChart/data.js b/Client/src/Components/Charts/AreaChart/data.js new file mode 100644 index 000000000..43efd165d --- /dev/null +++ b/Client/src/Components/Charts/AreaChart/data.js @@ -0,0 +1,59 @@ +const ObjectId = (string) => string; +const ISODate = (string) => string; + +// Helper to generate random percentage between 0.1 and 1.0 +const randomPercent = () => Number((Math.random() * 0.9 + 0.1).toFixed(2)); + +// Create base timestamp and increment by 5 minutes for each entry +const baseTime = new Date("2024-11-15T07:00:00.000Z"); +const checks = Array.from({ length: 20 }, (_, index) => { + const timestamp = new Date(baseTime.getTime() + index * 5 * 60 * 1000); + + return { + _id: ObjectId(`6736f4f449e23954c8b89a${index.toString(16).padStart(2, "0")}`), + monitorId: ObjectId("6736e6c2939f02e0ca519465"), + status: true, + responseTime: Math.floor(Math.random() * 50) + 80, // Random between 80-130ms + statusCode: 200, + message: "OK", + cpu: { + physical_core: 0, + logical_core: 0, + frequency: 0, + temperature: 0, + free_percent: 0, + usage_percent: randomPercent(), + _id: ObjectId(`6736f4f449e23954c8b89b${index.toString(16).padStart(2, "0")}`), + }, + memory: { + total_bytes: 0, + available_bytes: 0, + used_bytes: 0, + usage_percent: randomPercent(), + _id: ObjectId(`6736f4f449e23954c8b89c${index.toString(16).padStart(2, "0")}`), + }, + disk: [ + { + read_speed_bytes: 0, + write_speed_bytes: 0, + total_bytes: 0, + free_bytes: 0, + usage_percent: randomPercent(), + _id: ObjectId(`6736f4f449e23954c8b89d${index.toString(16).padStart(2, "0")}`), + }, + ], + host: { + os: "", + platform: "", + kernel_version: "", + _id: ObjectId(`6736f4f449e23954c8b89e${index.toString(16).padStart(2, "0")}`), + }, + errors: [], + expiry: ISODate(new Date(timestamp.getTime() + 24 * 60 * 60 * 1000).toISOString()), + createdAt: ISODate(timestamp.toISOString()), + updatedAt: ISODate(timestamp.toISOString()), + __v: 0, + }; +}); + +export default checks; From 924c799858078381c09f7b5fa034cf0d5ec26879 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 13:32:23 +0800 Subject: [PATCH 09/26] add and update chartUtils --- .../Components/Charts/Utils/chartUtils.jsx | 170 ++++++++++++++++++ .../Components/Charts/Utils/gradientUtils.jsx | 47 +++++ 2 files changed, 217 insertions(+) create mode 100644 Client/src/Components/Charts/Utils/chartUtils.jsx create mode 100644 Client/src/Components/Charts/Utils/gradientUtils.jsx diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx new file mode 100644 index 000000000..0d87421b8 --- /dev/null +++ b/Client/src/Components/Charts/Utils/chartUtils.jsx @@ -0,0 +1,170 @@ +import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; +import { useTheme } from "@mui/material"; +import { Text } from "recharts"; +import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { Box, Stack, Typography } from "@mui/material"; + +/** + * Custom tick component for rendering time with timezone. + * + * @param {Object} props - The properties object. + * @param {number} props.x - The x-coordinate for the tick. + * @param {number} props.y - The y-coordinate for the tick. + * @param {Object} props.payload - The payload object containing tick data. + * @param {number} props.index - The index of the tick. + * @returns {JSX.Element} The rendered tick component. + */ +export const TzTick = ({ x, y, payload, index }) => { + const theme = useTheme(); + + const uiTimezone = useSelector((state) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} + + ); +}; + +TzTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, +}; + +/** + * Custom tick component for rendering percentage values. + * + * @param {Object} props - The properties object. + * @param {number} props.x - The x-coordinate for the tick. + * @param {number} props.y - The y-coordinate for the tick. + * @param {Object} props.payload - The payload object containing tick data. + * @param {number} props.index - The index of the tick. + * @returns {JSX.Element|null} The rendered tick component or null for the first tick. + */ +export const PercentTick = ({ x, y, payload, index }) => { + const theme = useTheme(); + if (index === 0) return null; + return ( + + {`${payload?.value * 100}%`} + + ); +}; + +PercentTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, +}; + +/** + * Custom tooltip component for displaying infrastructure data. + * + * @param {Object} props - The properties object. + * @param {boolean} props.active - Indicates if the tooltip is active. + * @param {Array} props.payload - The payload array containing tooltip data. + * @param {string} props.label - The label for the tooltip. + * @param {string} props.yKey - The key for the y-axis data. + * @param {string} props.yLabel - The label for the y-axis data. + * @param {string} props.dotColor - The color of the dot in the tooltip. + * @returns {JSX.Element|null} The rendered tooltip component or null if inactive. + */ +export const InfrastructureTooltip = ({ + active, + payload, + label, + yKey, + yIdx = -1, + yLabel, + dotColor, +}) => { + const uiTimezone = useSelector((state) => state.ui.timezone); + const theme = useTheme(); + if (active && payload && payload.length) { + const [hardwareType, metric] = yKey.split("."); + return ( + + + {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} + + + + + + {yIdx >= 0 + ? `${yLabel} ${payload[0].payload[hardwareType][yIdx][metric] * 100}%` + : `${yLabel} ${payload[0].payload[hardwareType][metric] * 100}%`} + + + + + {/* Display original value */} + + ); + } + return null; +}; + +InfrastructureTooltip.propTypes = { + active: PropTypes.bool, + payload: PropTypes.array, + label: PropTypes.string, + yKey: PropTypes.string, + yIdx: PropTypes.number, + yLabel: PropTypes.string, + dotColor: PropTypes.string, +}; diff --git a/Client/src/Components/Charts/Utils/gradientUtils.jsx b/Client/src/Components/Charts/Utils/gradientUtils.jsx new file mode 100644 index 000000000..ef967b7ed --- /dev/null +++ b/Client/src/Components/Charts/Utils/gradientUtils.jsx @@ -0,0 +1,47 @@ +/** + * Creates an SVG gradient definition for use in charts + * @param {Object} params - The gradient parameters + * @param {string} [params.id="colorUv"] - Unique identifier for the gradient + * @param {string} params.startColor - Starting color of the gradient (hex, rgb, or color name) + * @param {string} params.endColor - Ending color of the gradient (hex, rgb, or color name) + * @param {number} [params.startOpacity=0.8] - Starting opacity (0-1) + * @param {number} [params.endOpacity=0] - Ending opacity (0-1) + * @param {('vertical'|'horizontal')} [params.direction="vertical"] - Direction of the gradient + * @returns {JSX.Element} SVG gradient definition element + * @example + * createCustomGradient({ + * startColor: "#1976D2", + * endColor: "#42A5F5", + * direction: "horizontal" + * }) + */ + +export const createGradient = ({ + id = "colorUv", + startColor, + endColor, + startOpacity = 0.8, + endOpacity = 0, + direction = "vertical", // or "horizontal" +}) => ( + + + + + + +); From 6a0d61b73e21864dfda780bfef7c6f716f431b4e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 13:34:43 +0800 Subject: [PATCH 10/26] Update Area Chart docs and proptype --- Client/src/Components/Charts/AreaChart/index.jsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx index ce4da9830..ee7e953f5 100644 --- a/Client/src/Components/Charts/AreaChart/index.jsx +++ b/Client/src/Components/Charts/AreaChart/index.jsx @@ -47,14 +47,22 @@ import { useTheme } from "@mui/material"; * data={data} * xKey="createdAt" * yKey="cpu.usage_percent" - * xTick={TzTick} - * yTick={PercentTick} + * xTick={} + * yTick={} * strokeColor="#8884d8" * fillColor="#8884d8" * gradient={true} * gradientStartColor="#8884d8" * gradientEndColor="#82ca9d" - * customTooltip={InfrastructureTooltip} + * customTooltip={({ active, payload, label }) => ( + * + * )} * /> * ); * }; @@ -138,7 +146,7 @@ CustomAreaChart.propTypes = { gradientDirection: PropTypes.string, gradientStartColor: PropTypes.string, gradientEndColor: PropTypes.string, - customTooltip: PropTypes.object, + customTooltip: PropTypes.func, }; export default CustomAreaChart; From a0c73469fab08f5c5580aea9c14ec0717e6a3662 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 13:35:00 +0800 Subject: [PATCH 11/26] Add area charts, fix dummy data --- .../src/Pages/Infrastructure/details/data.js | 35 ++++---- .../Pages/Infrastructure/details/index.jsx | 89 +++++++++++++++++++ 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/Client/src/Pages/Infrastructure/details/data.js b/Client/src/Pages/Infrastructure/details/data.js index 029de5203..f945ae525 100644 --- a/Client/src/Pages/Infrastructure/details/data.js +++ b/Client/src/Pages/Infrastructure/details/data.js @@ -1,26 +1,30 @@ function generateMonitorEntries(monitorId, count = 10, options = {}) { const defaultOptions = { timeRange: { - start: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago + start: new Date(Date.now() - 24 * 60 * 60 * 1000 * 20), end: new Date(), }, statusVariation: [true, false], responseTimeRange: [50, 500], - cpuUsageRange: [0, 100], - memoryUsageRange: [0, 100], - diskUsageRange: [0, 100], + cpuUsageRange: [0, 1], + memoryUsageRange: [0, 1], + diskUsageRange: [0, 1], }; const mergedOptions = { ...defaultOptions, ...options }; + const startTime = mergedOptions.timeRange.start.getTime(); + const endTime = mergedOptions.timeRange.end.getTime(); + const totalTimeSpan = endTime - startTime; - return Array.from({ length: count }, (_, index) => { - const createdAt = new Date( - mergedOptions.timeRange.start.getTime() + - (index * - (mergedOptions.timeRange.end.getTime() - - mergedOptions.timeRange.start.getTime())) / - count - ); + // Generate sorted random time points + const timePoints = Array.from({ length: count }, (_, index) => { + // Use a non-linear distribution to create more varied spacing + const progress = Math.pow(Math.random(), 2); // Bias towards earlier times + return startTime + progress * totalTimeSpan; + }).sort((a, b) => a - b); + + return timePoints.map((timestamp) => { + const createdAt = new Date(timestamp); return { _id: "123", @@ -34,7 +38,7 @@ function generateMonitorEntries(monitorId, count = 10, options = {}) { logical_core: randomInRange([4, 16]), frequency: randomInRange([10, 4000]), temperature: randomInRange([20, 90]), - free_percent: 100 - randomInRange(mergedOptions.cpuUsageRange), + free_percent: 1 - randomInRange(mergedOptions.cpuUsageRange), usage_percent: randomInRange(mergedOptions.cpuUsageRange), _id: "123", }, @@ -70,11 +74,12 @@ function generateMonitorEntries(monitorId, count = 10, options = {}) { }); } -// Helper functions +// Modify randomInRange to work with decimal ranges function randomInRange([min, max]) { - return Math.floor(Math.random() * (max - min + 1)) + min; + return Number((Math.random() * (max - min) + min).toFixed(2)); } +// ... rest of the code remains the same function randomFromArray(arr) { return arr[Math.floor(Math.random() * arr.length)]; } diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx index 9ff3c57f1..783db2040 100644 --- a/Client/src/Pages/Infrastructure/details/index.jsx +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -4,6 +4,12 @@ import Breadcrumbs from "../../../Components/Breadcrumbs"; import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import CustomGauge from "../../../Components/Charts/CustomGauge"; +import AreaChart from "../../../Components/Charts/AreaChart"; +import { + TzTick, + PercentTick, + InfrastructureTooltip, +} from "../../../Components/Charts/Utils/chartUtils"; const bytesToGB = (bytes) => { if (typeof bytes !== "number") return 0; @@ -92,6 +98,7 @@ const InfrastructureDetails = () => { { name: "infrastructure monitors", path: "/infrastructure" }, { name: "details", path: `/infrastructure/${monitorId}` }, ]; + console.log(testData); return ( @@ -160,6 +167,88 @@ const InfrastructureDetails = () => { ); })} + *": { + flexBasis: `calc(50% - ${theme.spacing(2)})`, + maxWidth: `calc(50% - ${theme.spacing(2)})`, + }, + }} + > + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.primary.main} + gradient={true} + gradientStartColor={theme.palette.primary.main} + gradientEndColor="#ffffff" + /> + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.primary.main} + gradient={true} + gradientStartColor={theme.palette.primary.main} + gradientEndColor="#ffffff" + /> + {latestCheck.disk.map((disk, idx) => { + return ( + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.primary.main} + gradient={true} + gradientStartColor={theme.palette.primary.main} + gradientEndColor="#ffffff" + /> + ); + })} + ); From e3c13d93ec6f05e6e9f37d0901e629decc9793e3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 13:40:52 +0800 Subject: [PATCH 12/26] Fix label proptype to allow for dates and numbers, add comments --- Client/src/Components/Charts/Utils/chartUtils.jsx | 6 +++++- Client/src/Pages/Infrastructure/details/index.jsx | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx index 0d87421b8..ad673e472 100644 --- a/Client/src/Components/Charts/Utils/chartUtils.jsx +++ b/Client/src/Components/Charts/Utils/chartUtils.jsx @@ -162,7 +162,11 @@ export const InfrastructureTooltip = ({ InfrastructureTooltip.propTypes = { active: PropTypes.bool, payload: PropTypes.array, - label: PropTypes.string, + label: PropTypes.oneOfType([ + PropTypes.instanceOf(Date), + PropTypes.string, + PropTypes.number, + ]), yKey: PropTypes.string, yIdx: PropTypes.number, yLabel: PropTypes.string, diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx index 783db2040..2922a20e9 100644 --- a/Client/src/Pages/Infrastructure/details/index.jsx +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -186,7 +186,7 @@ const InfrastructureDetails = () => { yKey="memory.usage_percent" customTooltip={({ active, payload, label }) => ( { yKey="cpu.usage_percent" customTooltip={({ active, payload, label }) => ( { gradientEndColor="#ffffff" /> {latestCheck.disk.map((disk, idx) => { + // disk is an array of disks, so we need to map over it return ( ( Date: Tue, 19 Nov 2024 13:54:51 +0800 Subject: [PATCH 13/26] Add a randomly generated gradientID to so each chart has a unique gradient --- Client/src/Components/Charts/AreaChart/index.jsx | 4 +++- Client/src/Components/Charts/Utils/gradientUtils.jsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx index ee7e953f5..4c2fc97ba 100644 --- a/Client/src/Components/Charts/AreaChart/index.jsx +++ b/Client/src/Components/Charts/AreaChart/index.jsx @@ -85,6 +85,7 @@ const CustomAreaChart = ({ customTooltip, }) => { const theme = useTheme(); + const gradientId = `gradient-${Math.random().toString(36).slice(2, 9)}`; return ( {gradient === true && createGradient({ + id: gradientId, startColor: gradientStartColor, endColor: gradientEndColor, direction: gradientDirection, @@ -116,7 +118,7 @@ const CustomAreaChart = ({ type="monotone" dataKey={dataKey} stroke={strokeColor} - fill={gradient === true ? "url(#colorUv)" : fillColor} + fill={gradient === true ? `url(#${gradientId})` : fillColor} /> {customTooltip ? ( diff --git a/Client/src/Components/Charts/Utils/gradientUtils.jsx b/Client/src/Components/Charts/Utils/gradientUtils.jsx index ef967b7ed..b5920374d 100644 --- a/Client/src/Components/Charts/Utils/gradientUtils.jsx +++ b/Client/src/Components/Charts/Utils/gradientUtils.jsx @@ -17,7 +17,7 @@ */ export const createGradient = ({ - id = "colorUv", + id, startColor, endColor, startOpacity = 0.8, From 0c194a04039f193f5bb590c90d62d377f93e52dc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 14:18:29 +0800 Subject: [PATCH 14/26] Change AreaChart responsive container height to 80% --- Client/src/Components/Charts/AreaChart/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx index 4c2fc97ba..4aa507940 100644 --- a/Client/src/Components/Charts/AreaChart/index.jsx +++ b/Client/src/Components/Charts/AreaChart/index.jsx @@ -89,7 +89,7 @@ const CustomAreaChart = ({ return ( Date: Tue, 19 Nov 2024 14:19:04 +0800 Subject: [PATCH 15/26] Add headers to charts, add multpilier for percentages, increase chart spacing --- .../Pages/Infrastructure/details/index.jsx | 174 ++++++++++-------- 1 file changed, 99 insertions(+), 75 deletions(-) diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx index 2922a20e9..c6c97d5f0 100644 --- a/Client/src/Pages/Infrastructure/details/index.jsx +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -23,6 +23,7 @@ const BaseBox = ({ children }) => { return ( { { name: "infrastructure monitors", path: "/infrastructure" }, { name: "details", path: `/infrastructure/${monitorId}` }, ]; - console.log(testData); return ( @@ -130,7 +130,7 @@ const InfrastructureDetails = () => { /> { gap={theme.spacing(8)} > { valueTwo={`${bytesToGB(latestCheck.memory.total_bytes)}GB`} /> { return ( { *": { - flexBasis: `calc(50% - ${theme.spacing(2)})`, - maxWidth: `calc(50% - ${theme.spacing(2)})`, + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, }, }} > - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.primary.main} - gradient={true} - gradientStartColor={theme.palette.primary.main} - gradientEndColor="#ffffff" - /> - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.primary.main} - gradient={true} - gradientStartColor={theme.palette.primary.main} - gradientEndColor="#ffffff" - /> + + + Memory usage + + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.primary.main} + gradient={true} + gradientStartColor={theme.palette.primary.main} //FE team HELP! Not sure what colors to use + gradientEndColor="#ffffff" // FE team HELP! + /> + + + + CPU usage + + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.success.main} // FE team HELP! + gradient={true} + fill={theme.palette.success.main} // FE team HELP! + gradientStartColor={theme.palette.success.main} + gradientEndColor="#ffffff" + /> + {latestCheck.disk.map((disk, idx) => { // disk is an array of disks, so we need to map over it return ( - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.primary.main} - gradient={true} - gradientStartColor={theme.palette.primary.main} - gradientEndColor="#ffffff" - /> + + + {`Disk${idx} usage`} + + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.warning.main} + gradient={true} + gradientStartColor={theme.palette.warning.main} + gradientEndColor="#ffffff" + /> + ); })} From 5a2c6a9aa7b3b948f9473dfb3f2a62bc6be00594 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 14:39:58 +0800 Subject: [PATCH 16/26] Add jsdocs and proptypes to Infrastructure Details Page --- .../Pages/Infrastructure/details/index.jsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx index c6c97d5f0..bf31e8d27 100644 --- a/Client/src/Pages/Infrastructure/details/index.jsx +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -10,7 +10,13 @@ import { PercentTick, InfrastructureTooltip, } from "../../../Components/Charts/Utils/chartUtils"; +import PropTypes from "prop-types"; +/** + * Converts bytes to gigabytes + * @param {number} bytes - Number of bytes to convert + * @returns {number} Converted value in gigabytes + */ const bytesToGB = (bytes) => { if (typeof bytes !== "number") return 0; if (bytes === 0) return 0; @@ -18,6 +24,12 @@ const bytesToGB = (bytes) => { return Number(GB.toFixed(0)); }; +/** + * Renders a base box with consistent styling + * @param {Object} props - Component properties + * @param {React.ReactNode} props.children - Child components to render inside the box + * @returns {React.ReactElement} Styled box component + */ const BaseBox = ({ children }) => { const theme = useTheme(); return ( @@ -38,6 +50,17 @@ const BaseBox = ({ children }) => { ); }; +BaseBox.propTypes = { + children: PropTypes.node.isRequired, +}; + +/** + * Renders a statistic box with a heading and subheading + * @param {Object} props - Component properties + * @param {string} props.heading - Primary heading text + * @param {string} props.subHeading - Secondary heading text + * @returns {React.ReactElement} Stat box component + */ const StatBox = ({ heading, subHeading }) => { return ( @@ -47,6 +70,22 @@ const StatBox = ({ heading, subHeading }) => { ); }; +StatBox.propTypes = { + heading: PropTypes.string.isRequired, + subHeading: PropTypes.string.isRequired, +}; + +/** + * Renders a gauge box with usage visualization + * @param {Object} props - Component properties + * @param {number} props.value - Percentage value for gauge + * @param {string} props.heading - Box heading + * @param {string} props.metricOne - First metric label + * @param {string} props.valueOne - First metric value + * @param {string} props.metricTwo - Second metric label + * @param {string} props.valueTwo - Second metric value + * @returns {React.ReactElement} Gauge box component + */ const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { const theme = useTheme(); return ( @@ -90,6 +129,19 @@ const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) ); }; +GaugeBox.propTypes = { + value: PropTypes.number.isRequired, + heading: PropTypes.string.isRequired, + metricOne: PropTypes.string.isRequired, + valueOne: PropTypes.string.isRequired, + metricTwo: PropTypes.string.isRequired, + valueTwo: PropTypes.string.isRequired, +}; + +/** + * Renders the infrastructure details page + * @returns {React.ReactElement} Infrastructure details page component + */ const InfrastructureDetails = () => { const theme = useTheme(); const { monitorId } = useParams(); From 9e7e13baea23a80306ee6c3a1a50090fc4cd0b51 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 15:28:57 +0800 Subject: [PATCH 17/26] truncate Gauge percentage to 2 decimal places --- Client/src/Components/Charts/CustomGauge/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Components/Charts/CustomGauge/index.jsx b/Client/src/Components/Charts/CustomGauge/index.jsx index d00ab7f5f..387efc64f 100644 --- a/Client/src/Components/Charts/CustomGauge/index.jsx +++ b/Client/src/Components/Charts/CustomGauge/index.jsx @@ -92,7 +92,7 @@ const CustomGauge = ({ fill: theme.typography.body2.color, }} > - {`${progress}%`} + {`${progress.toFixed(2)}%`} ); From 217ae50acc68ed2396d089218df0811f5bf3750d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 16:01:08 +0800 Subject: [PATCH 18/26] Fetch data from gist --- .../src/Pages/Infrastructure/details/data.js | 133 ------ .../Pages/Infrastructure/details/index.jsx | 388 ++++++++++-------- 2 files changed, 207 insertions(+), 314 deletions(-) delete mode 100644 Client/src/Pages/Infrastructure/details/data.js diff --git a/Client/src/Pages/Infrastructure/details/data.js b/Client/src/Pages/Infrastructure/details/data.js deleted file mode 100644 index f945ae525..000000000 --- a/Client/src/Pages/Infrastructure/details/data.js +++ /dev/null @@ -1,133 +0,0 @@ -function generateMonitorEntries(monitorId, count = 10, options = {}) { - const defaultOptions = { - timeRange: { - start: new Date(Date.now() - 24 * 60 * 60 * 1000 * 20), - end: new Date(), - }, - statusVariation: [true, false], - responseTimeRange: [50, 500], - cpuUsageRange: [0, 1], - memoryUsageRange: [0, 1], - diskUsageRange: [0, 1], - }; - - const mergedOptions = { ...defaultOptions, ...options }; - const startTime = mergedOptions.timeRange.start.getTime(); - const endTime = mergedOptions.timeRange.end.getTime(); - const totalTimeSpan = endTime - startTime; - - // Generate sorted random time points - const timePoints = Array.from({ length: count }, (_, index) => { - // Use a non-linear distribution to create more varied spacing - const progress = Math.pow(Math.random(), 2); // Bias towards earlier times - return startTime + progress * totalTimeSpan; - }).sort((a, b) => a - b); - - return timePoints.map((timestamp) => { - const createdAt = new Date(timestamp); - - return { - _id: "123", - monitorId: monitorId, - status: randomFromArray(mergedOptions.statusVariation), - responseTime: randomInRange(mergedOptions.responseTimeRange), - statusCode: randomStatusCode(), - message: randomMessage(), - cpu: { - physical_core: randomInRange([4, 8]), - logical_core: randomInRange([4, 16]), - frequency: randomInRange([10, 4000]), - temperature: randomInRange([20, 90]), - free_percent: 1 - randomInRange(mergedOptions.cpuUsageRange), - usage_percent: randomInRange(mergedOptions.cpuUsageRange), - _id: "123", - }, - memory: { - total_bytes: randomInRange([8, 32]) * 1024 * 1024 * 1024, - available_bytes: randomInRange([4, 16]) * 1024 * 1024 * 1024, - used_bytes: randomInRange([4, 16]) * 1024 * 1024 * 1024, - usage_percent: randomInRange(mergedOptions.memoryUsageRange), - _id: "123", - }, - disk: [ - { - read_speed_bytes: randomInRange([100, 1000]) * 1024 * 1024, - write_speed_bytes: randomInRange([100, 1000]) * 1024 * 1024, - total_bytes: randomInRange([100, 1000]) * 1024 * 1024 * 1024, - free_bytes: randomInRange([50, 500]) * 1024 * 1024 * 1024, - usage_percent: randomInRange(mergedOptions.diskUsageRange), - _id: "123", - }, - ], - host: { - os: randomOS(), - platform: randomPlatform(), - kernel_version: randomKernelVersion(), - _id: "123", - }, - errors: randomErrors(), - expiry: new Date(createdAt.getTime() + 365 * 24 * 60 * 60 * 1000), - createdAt: createdAt, - updatedAt: createdAt, - __v: 0, - }; - }); -} - -// Modify randomInRange to work with decimal ranges -function randomInRange([min, max]) { - return Number((Math.random() * (max - min) + min).toFixed(2)); -} - -// ... rest of the code remains the same -function randomFromArray(arr) { - return arr[Math.floor(Math.random() * arr.length)]; -} - -function randomStatusCode() { - const statusCodes = [200, 201, 204, 400, 401, 403, 404, 500, 502, 503]; - return randomFromArray(statusCodes); -} - -function randomMessage() { - const messages = [ - "OK", - "Created", - "No Content", - "Bad Request", - "Unauthorized", - "Forbidden", - "Not Found", - "Internal Server Error", - ]; - return randomFromArray(messages); -} - -function randomOS() { - const oss = ["Windows", "Linux", "macOS", "Ubuntu", "CentOS"]; - return randomFromArray(oss); -} - -function randomPlatform() { - const platforms = ["x64", "x86", "ARM", "ARM64"]; - return randomFromArray(platforms); -} - -function randomKernelVersion() { - return `${randomInRange([4, 6])}.${randomInRange([0, 20])}.${randomInRange([0, 100])}`; -} - -function randomErrors() { - const possibleErrors = [ - "Network timeout", - "Connection refused", - "SSL certificate error", - "DNS resolution failed", - "", - ]; - return Math.random() < 0.2 ? [randomFromArray(possibleErrors)] : []; -} - -// Usage -const monitorId = "123"; -export const monitorData = generateMonitorEntries(monitorId, 20); diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/details/index.jsx index bf31e8d27..40db34b80 100644 --- a/Client/src/Pages/Infrastructure/details/index.jsx +++ b/Client/src/Pages/Infrastructure/details/index.jsx @@ -1,10 +1,11 @@ -import { monitorData } from "./data"; import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import CustomGauge from "../../../Components/Charts/CustomGauge"; import AreaChart from "../../../Components/Charts/AreaChart"; +import axios from "axios"; import { TzTick, PercentTick, @@ -17,11 +18,18 @@ import PropTypes from "prop-types"; * @param {number} bytes - Number of bytes to convert * @returns {number} Converted value in gigabytes */ -const bytesToGB = (bytes) => { - if (typeof bytes !== "number") return 0; - if (bytes === 0) return 0; +const formatBytes = (bytes) => { + if (typeof bytes !== "number") return "0 GB"; + if (bytes === 0) return "0 GB"; + const GB = bytes / (1024 * 1024 * 1024); - return Number(GB.toFixed(0)); + const MB = bytes / (1024 * 1024); + + if (GB >= 1) { + return `${Number(GB.toFixed(0))} GB`; + } else { + return `${Number(MB.toFixed(0))} MB`; + } }; /** @@ -130,12 +138,12 @@ const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) }; GaugeBox.propTypes = { - value: PropTypes.number.isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, heading: PropTypes.string.isRequired, metricOne: PropTypes.string.isRequired, - valueOne: PropTypes.string.isRequired, + valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, metricTwo: PropTypes.string.isRequired, - valueTwo: PropTypes.string.isRequired, + valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }; /** @@ -145,189 +153,207 @@ GaugeBox.propTypes = { const InfrastructureDetails = () => { const theme = useTheme(); const { monitorId } = useParams(); - const testData = monitorData; - const latestCheck = testData[testData.length - 1]; const navList = [ { name: "infrastructure monitors", path: "/infrastructure" }, { name: "details", path: `/infrastructure/${monitorId}` }, ]; + const [monitor, setMonitor] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await axios.get("http://localhost:5000/api/v1/dummy-data", { + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + }, + }); + setMonitor(response.data.data); + } catch (error) { + console.error(error); + } + }; + fetchData(); + }, []); return ( - - - + monitor && ( + + - - - - - - - - - - {latestCheck.disk.map((disk, idx) => { - return ( - + + + + + + + + + + {monitor.checks[0].disk.map((disk, idx) => { + return ( + + ); + })} + + *": { + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, + }, + }} + > + + + Memory usage + + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.primary.main} + gradient={true} + gradientStartColor={theme.palette.primary.main} //FE team HELP! Not sure what colors to use + gradientEndColor="#ffffff" // FE team HELP! /> - ); - })} + + + + CPU usage + + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.success.main} // FE team HELP! + gradient={true} + fill={theme.palette.success.main} // FE team HELP! + gradientStartColor={theme.palette.success.main} + gradientEndColor="#ffffff" + /> + + {monitor?.checks?.[0]?.disk?.map((disk, idx) => { + // disk is an array of disks, so we need to map over it + return ( + + + {`Disk${idx} usage`} + + ( + + )} + xTick={} + yTick={} + strokeColor={theme.palette.warning.main} + gradient={true} + gradientStartColor={theme.palette.warning.main} + gradientEndColor="#ffffff" + /> + + ); + })} + - *": { - flexBasis: `calc(50% - ${theme.spacing(8)})`, - maxWidth: `calc(50% - ${theme.spacing(8)})`, - }, - }} - > - - - Memory usage - - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.primary.main} - gradient={true} - gradientStartColor={theme.palette.primary.main} //FE team HELP! Not sure what colors to use - gradientEndColor="#ffffff" // FE team HELP! - /> - - - - CPU usage - - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.success.main} // FE team HELP! - gradient={true} - fill={theme.palette.success.main} // FE team HELP! - gradientStartColor={theme.palette.success.main} - gradientEndColor="#ffffff" - /> - - {latestCheck.disk.map((disk, idx) => { - // disk is an array of disks, so we need to map over it - return ( - - - {`Disk${idx} usage`} - - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.warning.main} - gradient={true} - gradientStartColor={theme.palette.warning.main} - gradientEndColor="#ffffff" - /> - - ); - })} - - - + + ) ); }; From 7223ca366af960995a1a89f243f4beb87dfab86f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 19 Nov 2024 16:01:28 +0800 Subject: [PATCH 19/26] Add temporary dummy data route --- Server/index.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Server/index.js b/Server/index.js index 0e9811a43..996c4fd07 100644 --- a/Server/index.js +++ b/Server/index.js @@ -92,6 +92,23 @@ const startApp = async () => { app.use("/api/v1/queue", verifyJWT, queueRouter); app.use("/api/v1/status-page", statusPageRouter); + app.use("/api/v1/dummy-data", async (req, res) => { + try { + const response = await axios.get( + "https://gist.githubusercontent.com/ajhollid/9afa39410c7bbf52cc905f285a2225bf/raw/429a231a3559ebc95f6f488ed2c766bd7d6f46e5/dummyData.json", + { + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + }, + } + ); + return res.status(200).json(response.data); + } catch (error) { + return res.status(500).json({ message: error.message }); + } + }); + //health check app.use("/api/v1/healthy", (req, res) => { try { From dbe174d171a98f0007f96353ddc615cdca576de4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 Nov 2024 12:09:52 +0800 Subject: [PATCH 20/26] remove dummy from Area Chart --- .../src/Components/Charts/AreaChart/data.js | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 Client/src/Components/Charts/AreaChart/data.js diff --git a/Client/src/Components/Charts/AreaChart/data.js b/Client/src/Components/Charts/AreaChart/data.js deleted file mode 100644 index 43efd165d..000000000 --- a/Client/src/Components/Charts/AreaChart/data.js +++ /dev/null @@ -1,59 +0,0 @@ -const ObjectId = (string) => string; -const ISODate = (string) => string; - -// Helper to generate random percentage between 0.1 and 1.0 -const randomPercent = () => Number((Math.random() * 0.9 + 0.1).toFixed(2)); - -// Create base timestamp and increment by 5 minutes for each entry -const baseTime = new Date("2024-11-15T07:00:00.000Z"); -const checks = Array.from({ length: 20 }, (_, index) => { - const timestamp = new Date(baseTime.getTime() + index * 5 * 60 * 1000); - - return { - _id: ObjectId(`6736f4f449e23954c8b89a${index.toString(16).padStart(2, "0")}`), - monitorId: ObjectId("6736e6c2939f02e0ca519465"), - status: true, - responseTime: Math.floor(Math.random() * 50) + 80, // Random between 80-130ms - statusCode: 200, - message: "OK", - cpu: { - physical_core: 0, - logical_core: 0, - frequency: 0, - temperature: 0, - free_percent: 0, - usage_percent: randomPercent(), - _id: ObjectId(`6736f4f449e23954c8b89b${index.toString(16).padStart(2, "0")}`), - }, - memory: { - total_bytes: 0, - available_bytes: 0, - used_bytes: 0, - usage_percent: randomPercent(), - _id: ObjectId(`6736f4f449e23954c8b89c${index.toString(16).padStart(2, "0")}`), - }, - disk: [ - { - read_speed_bytes: 0, - write_speed_bytes: 0, - total_bytes: 0, - free_bytes: 0, - usage_percent: randomPercent(), - _id: ObjectId(`6736f4f449e23954c8b89d${index.toString(16).padStart(2, "0")}`), - }, - ], - host: { - os: "", - platform: "", - kernel_version: "", - _id: ObjectId(`6736f4f449e23954c8b89e${index.toString(16).padStart(2, "0")}`), - }, - errors: [], - expiry: ISODate(new Date(timestamp.getTime() + 24 * 60 * 60 * 1000).toISOString()), - createdAt: ISODate(timestamp.toISOString()), - updatedAt: ISODate(timestamp.toISOString()), - __v: 0, - }; -}); - -export default checks; From 8b739558024a7cbabed373018d767241fdbf21b0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 Nov 2024 12:10:25 +0800 Subject: [PATCH 21/26] truncate decimals in tool top --- Client/src/Components/Charts/Utils/chartUtils.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx index ad673e472..0922d8b7d 100644 --- a/Client/src/Components/Charts/Utils/chartUtils.jsx +++ b/Client/src/Components/Charts/Utils/chartUtils.jsx @@ -146,8 +146,8 @@ export const InfrastructureTooltip = ({ sx={{ opacity: 0.8 }} > {yIdx >= 0 - ? `${yLabel} ${payload[0].payload[hardwareType][yIdx][metric] * 100}%` - : `${yLabel} ${payload[0].payload[hardwareType][metric] * 100}%`} + ? `${yLabel} ${(payload[0].payload[hardwareType][yIdx][metric] * 100).toFixed(2)}%` + : `${yLabel} ${(payload[0].payload[hardwareType][metric] * 100).toFixed(2)}%`} From b1449d3587a38d63bd8c420a8df6797f1fef229c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 Nov 2024 12:11:03 +0800 Subject: [PATCH 22/26] refactor details page to use reduce code duplication, caluclate area chart height --- .../src/Components/Charts/AreaChart/index.jsx | 11 +- .../Pages/Infrastructure/details/index.jsx | 299 +++++++++--------- 2 files changed, 160 insertions(+), 150 deletions(-) diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx index 4aa507940..d05dbb14a 100644 --- a/Client/src/Components/Charts/AreaChart/index.jsx +++ b/Client/src/Components/Charts/AreaChart/index.jsx @@ -10,7 +10,7 @@ import { import { createGradient } from "../Utils/gradientUtils"; import PropTypes from "prop-types"; import { useTheme } from "@mui/material"; - +import { useId } from "react"; /** * CustomAreaChart component for rendering an area chart with optional gradient and custom ticks. * @@ -83,13 +83,16 @@ const CustomAreaChart = ({ gradientStartColor, gradientEndColor, customTooltip, + height = "100%", }) => { const theme = useTheme(); - const gradientId = `gradient-${Math.random().toString(36).slice(2, 9)}`; + const uniqueId = useId(); + const gradientId = `gradient-${uniqueId}`; return ( - {customTooltip ? ( { * Renders a base box with consistent styling * @param {Object} props - Component properties * @param {React.ReactNode} props.children - Child components to render inside the box + * @param {Object} props.sx - Additional styling for the box * @returns {React.ReactElement} Styled box component */ -const BaseBox = ({ children }) => { +const BaseBox = ({ children, sx = {} }) => { const theme = useTheme(); return ( {children} @@ -60,6 +65,7 @@ const BaseBox = ({ children }) => { BaseBox.propTypes = { children: PropTypes.node.isRequired, + sx: PropTypes.object, }; /** @@ -159,6 +165,18 @@ const InfrastructureDetails = () => { ]; const [monitor, setMonitor] = useState(null); + // These calculations are needed because ResponsiveContainer + // doesn't take padding of parent/siblings into account + // when calculating height. + const chartContainerHeight = 300; + const totalChartContainerPadding = + parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2; + const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2; + const areaChartHeight = + (chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95; + // end height calculations + + // Fetch data useEffect(() => { const fetchData = async () => { try { @@ -176,6 +194,86 @@ const InfrastructureDetails = () => { fetchData(); }, []); + const statBoxConfigs = [ + { + id: 0, + heading: "CPU", + subHeading: `${monitor?.checks[0]?.cpu?.physical_core} cores`, + }, + { + id: 1, + heading: "Memory", + subHeading: formatBytes(monitor?.checks[0]?.memory?.total_bytes), + }, + { + id: 2, + heading: "Disk", + subHeading: formatBytes(monitor?.checks[0]?.disk[0]?.total_bytes), + }, + { id: 3, heading: "Uptime", subHeading: "100%" }, + { + id: 4, + heading: "Status", + subHeading: monitor?.status === true ? "Active" : "Inactive", + }, + ]; + + const gaugeBoxConfigs = [ + { + type: "memory", + value: monitor?.checks[0]?.memory?.usage_percent * 100, + heading: "Memory Usage", + metricOne: "Used", + valueOne: formatBytes(monitor?.checks[0]?.memory?.used_bytes), + metricTwo: "Total", + valueTwo: formatBytes(monitor?.checks[0]?.memory?.total_bytes), + }, + { + type: "cpu", + value: monitor?.checks[0]?.cpu?.usage_percent * 100, + heading: "CPU Usage", + metricOne: "Cores", + valueOne: monitor?.checks[0]?.cpu?.physical_core, + metricTwo: "Frequency", + valueTwo: `${(monitor?.checks[0]?.cpu?.frequency / 1000).toFixed(2)} Ghz`, + }, + ...(monitor?.checks[0]?.disk ?? []).map((disk, idx) => ({ + type: "disk", + diskIndex: idx, + value: disk.usage_percent * 100, + heading: `Disk${idx} usage`, + metricOne: "Used", + valueOne: formatBytes(disk.total_bytes - disk.free_bytes), + metricTwo: "Total", + valueTwo: formatBytes(disk.total_bytes), + })), + ]; + + const areaChartConfigs = [ + { + type: "memory", + dataKey: "memory.usage_percent", + heading: "Memory usage", + strokeColor: theme.palette.primary.main, + yLabel: "Memory Usage", + }, + { + type: "cpu", + dataKey: "cpu.usage_percent", + heading: "CPU usage", + strokeColor: theme.palette.success.main, + yLabel: "CPU Usage", + }, + ...(monitor?.checks?.[0]?.disk?.map((disk, idx) => ({ + type: "disk", + diskIndex: idx, + dataKey: `disk[${idx}].usage_percent`, + heading: `Disk${idx} usage`, + strokeColor: theme.palette.warning.main, + yLabel: "Disk Usage", + })) || []), + ]; + return ( monitor && ( @@ -189,64 +287,32 @@ const InfrastructureDetails = () => { direction="row" gap={theme.spacing(8)} > - - - - - + {statBoxConfigs.map((statBox) => ( + + ))} - - - {monitor.checks[0].disk.map((disk, idx) => { - return ( - - ); - })} + {gaugeBoxConfigs.map((config) => ( + + ))} { }, }} > - - - Memory usage - - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.primary.main} - gradient={true} - gradientStartColor={theme.palette.primary.main} //FE team HELP! Not sure what colors to use - gradientEndColor="#ffffff" // FE team HELP! - /> - - - - CPU usage - - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.success.main} // FE team HELP! - gradient={true} - fill={theme.palette.success.main} // FE team HELP! - gradientStartColor={theme.palette.success.main} - gradientEndColor="#ffffff" - /> - - {monitor?.checks?.[0]?.disk?.map((disk, idx) => { - // disk is an array of disks, so we need to map over it - return ( - - - {`Disk${idx} usage`} - - ( - - )} - xTick={} - yTick={} - strokeColor={theme.palette.warning.main} - gradient={true} - gradientStartColor={theme.palette.warning.main} - gradientEndColor="#ffffff" - /> - - ); - })} + {areaChartConfigs.map((config) => ( + + + {config.heading} + + ( + + )} + xTick={} + yTick={} + strokeColor={config.strokeColor} + gradient={true} + gradientStartColor={config.strokeColor} + gradientEndColor="#ffffff" + /> + + ))} From 9bbcaaf156b195c2a65d46a34c261219de33d668 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 Nov 2024 12:13:46 +0800 Subject: [PATCH 23/26] memoize calculations in SVG gauge --- Client/src/Components/Charts/CustomGauge/index.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Client/src/Components/Charts/CustomGauge/index.jsx b/Client/src/Components/Charts/CustomGauge/index.jsx index 387efc64f..f3ad15b0b 100644 --- a/Client/src/Components/Charts/CustomGauge/index.jsx +++ b/Client/src/Components/Charts/CustomGauge/index.jsx @@ -1,5 +1,5 @@ import { useTheme } from "@emotion/react"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; import "./index.css"; @@ -31,9 +31,14 @@ const CustomGauge = ({ strokeWidth = 15, }) => { // Calculate the length of the stroke for the circle - const circumference = 2 * Math.PI * radius; - const totalSize = radius * 2 + strokeWidth * 2; // This is the total size of the SVG, needed for the viewBox - const strokeLength = (progress / 100) * circumference; + const { circumference, totalSize, strokeLength } = useMemo( + () => ({ + circumference: 2 * Math.PI * radius, + totalSize: radius * 2 + strokeWidth * 2, + strokeLength: (progress / 100) * (2 * Math.PI * radius), + }), + [radius, strokeWidth, progress] + ); const [offset, setOffset] = useState(circumference); const theme = useTheme(); From 1f5b5dd612237dbe6df926c31195d5e3ea9e41fe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 Nov 2024 12:16:27 +0800 Subject: [PATCH 24/26] details -> Details --- Client/src/App.jsx | 2 +- Client/src/Pages/Infrastructure/{details => Details}/index.jsx | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Client/src/Pages/Infrastructure/{details => Details}/index.jsx (100%) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 03c406a6c..772c1aa45 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -39,7 +39,7 @@ import { getAppSettings } from "./Features/Settings/settingsSlice"; import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Infrastructure } from "./Pages/Infrastructure"; -import InfrastructureDetails from "./Pages/Infrastructure/details"; +import InfrastructureDetails from "./Pages/Infrastructure/Details"; function App() { const AdminCheckedRegister = withAdminCheck(Register); const MonitorsWithAdminProp = withAdminProp(Monitors); diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx similarity index 100% rename from Client/src/Pages/Infrastructure/details/index.jsx rename to Client/src/Pages/Infrastructure/Details/index.jsx From 8bbb0012db706ff707bed7daf05c688f708bf020 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 20 Nov 2024 12:42:06 +0800 Subject: [PATCH 25/26] Add monitor information at top of page --- .../Pages/Infrastructure/Details/index.jsx | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index 100e785c8..4fb547a5a 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -5,6 +5,9 @@ import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import CustomGauge from "../../../Components/Charts/CustomGauge"; import AreaChart from "../../../Components/Charts/AreaChart"; +import PulseDot from "../../../Components/Animated/PulseDot"; +import useUtils from "../../Monitors/utils"; +import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils"; import axios from "axios"; import { TzTick, @@ -164,7 +167,7 @@ const InfrastructureDetails = () => { { name: "details", path: `/infrastructure/${monitorId}` }, ]; const [monitor, setMonitor] = useState(null); - + const { statusColor, determineState } = useUtils(); // These calculations are needed because ResponsiveContainer // doesn't take padding of parent/siblings into account // when calculating height. @@ -283,6 +286,30 @@ const InfrastructureDetails = () => { gap={theme.spacing(10)} mt={theme.spacing(10)} > + + + + + + {monitor.name} + + {monitor.url || "..."} + + + Checking every {formatDurationRounded(monitor?.interval)} + + + Last checked {formatDurationSplit(monitor?.lastChecked).time}{" "} + {formatDurationSplit(monitor?.lastChecked).format} ago + + Date: Wed, 20 Nov 2024 12:55:58 +0800 Subject: [PATCH 26/26] extract tooltip percentage formatting --- .../Components/Charts/Utils/chartUtils.jsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx index 0922d8b7d..043203525 100644 --- a/Client/src/Components/Charts/Utils/chartUtils.jsx +++ b/Client/src/Components/Charts/Utils/chartUtils.jsx @@ -74,6 +74,22 @@ PercentTick.propTypes = { index: PropTypes.number, }; +/** + * Converts a decimal value to a formatted percentage string. + * + * @param {number} value - The decimal value to convert (e.g., 0.75) + * @returns {string} Formatted percentage string (e.g., "75.00%") or original input if not a number + * + * @example + * getFormattedPercentage(0.7543) // Returns "75.43%" + * getFormattedPercentage(1) // Returns "100.00%" + * getFormattedPercentage("test") // Returns "test" + */ +const getFormattedPercentage = (value) => { + if (typeof value !== "number") return value; + return `${(value * 100).toFixed(2)}.%`; +}; + /** * Custom tooltip component for displaying infrastructure data. * @@ -146,8 +162,8 @@ export const InfrastructureTooltip = ({ sx={{ opacity: 0.8 }} > {yIdx >= 0 - ? `${yLabel} ${(payload[0].payload[hardwareType][yIdx][metric] * 100).toFixed(2)}%` - : `${yLabel} ${(payload[0].payload[hardwareType][metric] * 100).toFixed(2)}%`} + ? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}` + : `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][metric])}`}