From 1cbec8220dff3394293391d9eebc016f3824711f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 10 Jul 2025 16:08:04 -0700 Subject: [PATCH 01/27] add diagnostic page --- .../Components/Charts/CustomGauge/index.jsx | 4 +- client/src/Hooks/logHooks.js | 77 ++++++++++++++++++- client/src/Hooks/queueHooks.js | 57 -------------- .../Diagnostics/components/gauges/index.jsx | 74 ++++++++++++++++++ .../Diagnostics/components/gauges/utils.js | 4 + client/src/Pages/Logs/Diagnostics/index.jsx | 30 ++++++++ client/src/Pages/Logs/Logs/index.jsx | 2 +- client/src/Pages/Logs/Queue/index.jsx | 2 +- client/src/Pages/Logs/index.jsx | 5 +- client/src/Utils/NetworkService.js | 8 ++ client/src/locales/en.json | 6 +- server/controllers/diagnosticController.js | 73 ++++++++++++++++++ server/routes/diagnosticRoute.js | 1 + 13 files changed, 279 insertions(+), 64 deletions(-) delete mode 100644 client/src/Hooks/queueHooks.js create mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx create mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/utils.js create mode 100644 client/src/Pages/Logs/Diagnostics/index.jsx diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 3c7e13610..359b5b328 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -54,8 +54,8 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = const fillColor = progressWithinRange > threshold - ? theme.palette.error.lowContrast // CAIO_REVIEW - : theme.palette.accent.main; // CAIO_REVIEW + ? theme.palette.error.lowContrast + : theme.palette.accent.main; return ( { return [logs, isLoading, error]; }; -export { useFetchLogs }; +const useFetchQueueData = (trigger) => { + const [jobs, setJobs] = useState(undefined); + const [metrics, setMetrics] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + const fetchJobs = async () => { + try { + setIsLoading(true); + const response = await networkService.getQueueData(); + if (response.status === 200) { + setJobs(response.data.data.jobs); + setMetrics(response.data.data.metrics); + } + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + fetchJobs(); + }, [trigger]); + + return [jobs, metrics, isLoading, error]; +}; + +const useFlushQueue = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + const flushQueue = async (trigger, setTrigger) => { + try { + setIsLoading(true); + await networkService.flushQueue(); + createToast({ + body: "Queue flushed", + }); + } catch (error) { + setError(error); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + setTrigger(!trigger); + } + }; + return [flushQueue, isLoading, error]; +}; + +const useFetchDiagnostics = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const [diagnostics, setDiagnostics] = useState(undefined); + + useEffect(() => { + const fetchDiagnostics = async () => { + try { + setIsLoading(true); + const response = await networkService.getDiagnostics(); + setDiagnostics(response.data.data); + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + fetchDiagnostics(); + }, []); + + return [diagnostics, isLoading, error]; +}; + +export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics }; diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js deleted file mode 100644 index f7a25b3d3..000000000 --- a/client/src/Hooks/queueHooks.js +++ /dev/null @@ -1,57 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -const useFetchQueueData = (trigger) => { - const [jobs, setJobs] = useState(undefined); - const [metrics, setMetrics] = useState(undefined); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - const fetchJobs = async () => { - try { - setIsLoading(true); - const response = await networkService.getQueueData(); - if (response.status === 200) { - setJobs(response.data.data.jobs); - setMetrics(response.data.data.metrics); - } - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }; - - fetchJobs(); - }, [trigger]); - - return [jobs, metrics, isLoading, error]; -}; - -const useFlushQueue = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - const flushQueue = async (trigger, setTrigger) => { - try { - setIsLoading(true); - await networkService.flushQueue(); - createToast({ - body: "Queue flushed", - }); - } catch (error) { - setError(error); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setTrigger(!trigger); - } - }; - return [flushQueue, isLoading, error]; -}; - -export { useFetchQueueData, useFlushQueue }; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx new file mode 100644 index 000000000..21d9a35d8 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -0,0 +1,74 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Gauge from "../../../../../Components/Charts/CustomGauge"; +import Typography from "@mui/material/Typography"; + +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +import { getPercentage } from "./utils"; + +const GaugeBox = ({ title, subtitle, children }) => { + const theme = useTheme(); + return ( + + {children} + + {title} + {subtitle} + + ); +}; + +const Gauges = ({ diagnostics }) => { + const heapTotalSize = getPercentage( + diagnostics?.v8HeapStats?.totalHeapSizeMb, + diagnostics?.v8HeapStats?.heapSizeLimitMb + ); + + const heapUsedSize = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeMb, + diagnostics?.v8HeapStats?.heapSizeLimitMb + ); + + const actualHeapUsed = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeMb, + diagnostics?.v8HeapStats?.totalHeapSizeMb + ); + const theme = useTheme(); + + return ( + + + + + + + + + + + + ); +}; + +Gauges.propTypes = { + diagnostics: PropTypes.object.isRequired, +}; + +export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js new file mode 100644 index 000000000..bdc570a3b --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js @@ -0,0 +1,4 @@ +export const getPercentage = (value, total) => { + if (!value || !total) return 0; + return (value / total) * 100; +}; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx new file mode 100644 index 000000000..9d7ee4639 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -0,0 +1,30 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Gauges from "./components/gauges"; + +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks"; + +const Diagnostics = () => { + // Local state + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const [diagnostics, isLoading, error] = useFetchDiagnostics(); + console.log(diagnostics); + // Setup + return ( + + + {t("diagnosticsPage.description")} + + + + ); +}; + +export default Diagnostics; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index 6c5e1a301..d8bd323a3 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -51,7 +51,7 @@ const Logs = () => { return ( - {t("logsPage.description")} + {t("logsPage.description")} { const theme = useTheme(); // Local state - const [value, setValue] = useState(0); + const [value, setValue] = useState(2); // Handlers const handleChange = (event, newValue) => { @@ -31,9 +32,11 @@ const Logs = () => { > + {value === 0 && } {value === 1 && } + {value === 2 && } ); }; diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 7fba2ce18..895d90d30 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -1091,6 +1091,14 @@ class NetworkService { }, }); } + + async getDiagnostics() { + return this.axiosInstance.get(`/diagnostic/system`, { + headers: { + "Content-Type": "application/json", + }, + }); + } } export default NetworkService; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ddc07f166..54f9600e2 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -451,13 +451,17 @@ }, "tabs": { "logs": "Server logs", - "queue": "Job queue" + "queue": "Job queue", + "diagnostics": "Diagnostics" }, "title": "Logs", "toast": { "fetchLogsSuccess": "Logs fetched successfully" } }, + "diagnosticsPage": { + "description": "System diagnostics" + }, "low": "low", "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index 8fa5fa42b..c4223dc04 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -1,6 +1,14 @@ import { handleError } from "./controllerUtils.js"; +import v8 from "v8"; +import os from "os"; + const SERVICE_NAME = "diagnosticController"; +const obs = new PerformanceObserver((items) => { + const entry = items.getEntries()[0]; + performance.clearMarks(); +}); +obs.observe({ entryTypes: ["measure"] }); class DiagnosticController { constructor(db) { this.db = db; @@ -44,5 +52,70 @@ class DiagnosticController { next(handleError(error, SERVICE_NAME, "getDbStats")); } } + + async getSystemStats(req, res, next) { + try { + // Memory Usage + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + + const osStats = { + freeMemoryMb: freeMemory / 1024 / 1024, // MB + totalMemoryMb: totalMemory / 1024 / 1024, // MB + }; + + const used = process.memoryUsage(); + const memoryUsage = {}; + for (let key in used) { + memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB + } + + // CPU Usage + const cpuUsage = process.cpuUsage(); + const cpuMetrics = { + userUsageMs: cpuUsage.user / 1000, // ms + systemUsageMs: cpuUsage.system / 1000, // ms + }; + + // V8 Heap Statistics + const heapStats = v8.getHeapStatistics(); + const v8Metrics = { + totalHeapSizeMb: heapStats.total_heap_size / 1024 / 1024, // MB + usedHeapSizeMb: heapStats.used_heap_size / 1024 / 1024, // MB + heapSizeLimitMb: heapStats.heap_size_limit / 1024 / 1024, // MB + }; + + // Event Loop Delay + let eventLoopDelay = 0; + performance.mark("start"); + await new Promise((resolve) => setTimeout(resolve, 0)); + performance.mark("end"); + performance.measure("eventLoopDelay", "start", "end"); + const entries = performance.getEntriesByName("eventLoopDelay"); + if (entries.length > 0) { + eventLoopDelay = entries[0].duration; + } + + // Uptime + const uptime = process.uptime(); // seconds + + // Combine Metrics + const diagnostics = { + osStats, + memoryUsage, + cpuUsage: cpuMetrics, + v8HeapStats: v8Metrics, + eventLoopDelayMs: eventLoopDelay, + uptimeSeconds: uptime, + }; + + return res.success({ + msg: "OK", + data: diagnostics, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMemoryUsage")); + } + } } export default DiagnosticController; diff --git a/server/routes/diagnosticRoute.js b/server/routes/diagnosticRoute.js index 616d01ccf..0d8f4df84 100755 --- a/server/routes/diagnosticRoute.js +++ b/server/routes/diagnosticRoute.js @@ -13,6 +13,7 @@ class DiagnosticRoutes { ); this.router.post("/db/stats", this.diagnosticController.getDbStats); + this.router.get("/system", this.diagnosticController.getSystemStats); } getRouter() { From 2c1a01bb6ac50b5fd4ca6fdd167a9ab741ab6405 Mon Sep 17 00:00:00 2001 From: shanika Jayawardane Date: Fri, 11 Jul 2025 07:44:02 -0600 Subject: [PATCH 02/27] add the All select option to monitor maintenance window for all monitors same as status pages --- .../Components/MonitorList/index.jsx | 107 ++++++++++++++++++ .../Maintenance/CreateMaintenance/index.jsx | 55 ++++++++- 2 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx new file mode 100644 index 000000000..36d6594dc --- /dev/null +++ b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx @@ -0,0 +1,107 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; +import DeleteIcon from "@mui/icons-material/Delete"; + +// Utils +import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; +import { useTheme } from "@emotion/react"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; +const MonitorListItem = ({ + monitor, + innerRef, + draggableProps, + dragHandleProps, + onDelete, +}) => { + const theme = useTheme(); + return ( + + + {monitor.name} + { + onDelete(monitor); + }} + /> + + ); +}; + +const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { + const onDelete = (monitorToDelete) => { + const newMonitors = selectedMonitors.filter( + (monitor) => monitor._id !== monitorToDelete._id + ); + setSelectedMonitors(newMonitors); + }; + const reorder = (list, startIndex, endIndex) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; + }; + + const onDragEnd = (result) => { + // dropped outside the list + if (!result.destination) { + return; + } + + const reorderedMonitors = reorder( + selectedMonitors, + result.source.index, + result.destination.index + ); + + setSelectedMonitors(reorderedMonitors); + }; + + return ( + + + {(provided, snapshot) => ( + + {selectedMonitors?.map((monitor, index) => ( + + {(provided, snapshot) => ( + + )} + + ))} + {provided.placeholder} + + )} + + + ); +}; + +export default MonitorList; diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index e71c7112c..c4e839e02 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -9,6 +9,8 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; import { maintenanceWindowValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; +import MonitorList from "./Components/MonitorList"; +import Checkbox from "../../../Components/Inputs/Checkbox"; import dayjs from "dayjs"; import Select from "../../../Components/Inputs/Select"; @@ -129,6 +131,7 @@ const CreateMaintenance = () => { monitors: [], }); const [errors, setErrors] = useState({}); + const [selectedMonitors, setSelectedMonitors] = useState([]); useEffect(() => { const fetchMonitors = async () => { @@ -165,6 +168,7 @@ const CreateMaintenance = () => { durationUnit, monitors: monitor ? [monitor] : [], }); + setSelectedMonitors(monitor ? [monitor] : []); } catch (error) { createToast({ body: "Failed to fetch data" }); logger.error("Failed to fetch monitors", error); @@ -175,6 +179,11 @@ const CreateMaintenance = () => { fetchMonitors(); }, [user]); + // Sync form.monitors with selectedMonitors + useEffect(() => { + setForm((prev) => ({ ...prev, monitors: selectedMonitors })); + }, [selectedMonitors]); + const handleSearch = (value) => { setSearch(value); }; @@ -212,6 +221,33 @@ const CreateMaintenance = () => { }); }; + const handleMonitorsChange = (selected) => { + if (selected.some((m) => m._id === "__all__")) { + if (selectedMonitors.length !== monitors.length) { + setSelectedMonitors(monitors); + } else { + setSelectedMonitors([]); + } + } else { + // Ensure all are objects + const selectedObjs = selected + .map((sel) => + typeof sel === "string" ? monitors.find((m) => m._id === sel) : sel + ) + .filter(Boolean); + setSelectedMonitors(selectedObjs); + } + const { error } = maintenanceWindowValidation.validate( + { monitors: selected }, + { abortEarly: false } + ); + setErrors((prev) => { + return buildErrors(prev, "monitors", error); + }); + }; + + const allSelected = selectedMonitors.length === monitors.length && monitors.length > 0; + const handleSubmit = async () => { if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return; // Build timestamp for maintenance window from startDate and startTime @@ -267,6 +303,10 @@ const CreateMaintenance = () => { } }; + // Add Select All option to the dropdown + const selectAllOption = { _id: "__all__", name: "Select All" }; + const monitorOptions = [selectAllOption, ...monitors]; + return ( { label={t("addMonitors")} multiple={true} isAdorned={false} - options={monitors ? monitors : []} + options={monitorOptions} filteredBy="name" secondaryLabel={"type"} inputValue={search} - value={form.monitors} - handleInputChange={handleSearch} - handleChange={handleSelectMonitors} + value={selectedMonitors} + handleInputChange={setSearch} + handleChange={handleMonitorsChange} error={errors["monitors"]} - disabled={maintenanceWindowId !== undefined} + disabled={form.isAllMonitors} + /> + + Date: Fri, 11 Jul 2025 07:59:12 -0600 Subject: [PATCH 03/27] add the translation to selectAll --- client/src/Pages/Maintenance/CreateMaintenance/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index c4e839e02..0be00e8da 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -304,7 +304,7 @@ const CreateMaintenance = () => { }; // Add Select All option to the dropdown - const selectAllOption = { _id: "__all__", name: "Select All" }; + const selectAllOption = { _id: "__all__", name: t("selectAll") }; const monitorOptions = [selectAllOption, ...monitors]; return ( From b1b4fd3f6406fa83468a8726bfe45a35e2e9a933 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 11 Jul 2025 11:23:36 -0700 Subject: [PATCH 04/27] add 1s cpu usage metric, return standard units of ms and bytes --- server/controllers/diagnosticController.js | 43 +++++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index c4223dc04..4410a28a5 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -53,15 +53,34 @@ class DiagnosticController { } } - async getSystemStats(req, res, next) { + async getCPUUsage() { + try { + const startUsage = process.cpuUsage(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const endUsage = process.cpuUsage(startUsage); + const cpuUsage = { + userUsageMs: endUsage.user / 1000, + systemUsageMs: endUsage.system / 1000, + usagePercentage: ((endUsage.user + endUsage.system) / 1000 / 1000) * 100, + }; + return cpuUsage; + } catch (error) { + return { + userUsageMs: 0, + systemUsageMs: 0, + }; + } + } + + getSystemStats = async (req, res, next) => { try { // Memory Usage const totalMemory = os.totalmem(); const freeMemory = os.freemem(); const osStats = { - freeMemoryMb: freeMemory / 1024 / 1024, // MB - totalMemoryMb: totalMemory / 1024 / 1024, // MB + freeMemoryBytes: freeMemory, // bytes + totalMemoryBytes: totalMemory, // bytes }; const used = process.memoryUsage(); @@ -71,18 +90,14 @@ class DiagnosticController { } // CPU Usage - const cpuUsage = process.cpuUsage(); - const cpuMetrics = { - userUsageMs: cpuUsage.user / 1000, // ms - systemUsageMs: cpuUsage.system / 1000, // ms - }; + const cpuMetrics = await this.getCPUUsage(); // V8 Heap Statistics const heapStats = v8.getHeapStatistics(); const v8Metrics = { - totalHeapSizeMb: heapStats.total_heap_size / 1024 / 1024, // MB - usedHeapSizeMb: heapStats.used_heap_size / 1024 / 1024, // MB - heapSizeLimitMb: heapStats.heap_size_limit / 1024 / 1024, // MB + totalHeapSizeBytes: heapStats.total_heap_size, // bytes + usedHeapSizeBytes: heapStats.used_heap_size, // bytes + heapSizeLimitBytes: heapStats.heap_size_limit, // bytes }; // Event Loop Delay @@ -97,7 +112,7 @@ class DiagnosticController { } // Uptime - const uptime = process.uptime(); // seconds + const uptimeMs = process.uptime() * 1000; // ms // Combine Metrics const diagnostics = { @@ -106,7 +121,7 @@ class DiagnosticController { cpuUsage: cpuMetrics, v8HeapStats: v8Metrics, eventLoopDelayMs: eventLoopDelay, - uptimeSeconds: uptime, + uptimeMs, }; return res.success({ @@ -116,6 +131,6 @@ class DiagnosticController { } catch (error) { next(handleError(error, SERVICE_NAME, "getMemoryUsage")); } - } + }; } export default DiagnosticController; From 5b395b96cd5f7001dee9d62c5a20e03471b6993f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 11 Jul 2025 11:23:50 -0700 Subject: [PATCH 05/27] implemet stats --- .../Components/Charts/CustomGauge/index.jsx | 35 ++++++- .../Diagnostics/components/gauges/index.jsx | 73 ++++++++++---- .../Diagnostics/components/gauges/utils.js | 4 - .../Diagnostics/components/stats/index.jsx | 97 +++++++++++++++++++ client/src/Pages/Logs/Diagnostics/index.jsx | 20 +++- .../src/Pages/Logs/Diagnostics/utils/utils.js | 16 +++ client/src/Pages/Logs/Logs/index.jsx | 4 + .../Logs/Queue/components/Metrics/index.jsx | 27 +++--- client/src/Pages/Logs/Queue/index.jsx | 69 +++++++------ client/src/Utils/timeUtils.js | 4 + client/src/locales/en.json | 22 ++++- 11 files changed, 294 insertions(+), 77 deletions(-) delete mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/utils.js create mode 100644 client/src/Pages/Logs/Diagnostics/components/stats/index.jsx create mode 100644 client/src/Pages/Logs/Diagnostics/utils/utils.js diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 359b5b328..d9718d72c 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -1,8 +1,10 @@ import { useTheme } from "@emotion/react"; import { useEffect, useState, useMemo } from "react"; +import Stack from "@mui/material/Stack"; import { Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; import "./index.css"; +import CircularProgress from "@mui/material/CircularProgress"; const MINIMUM_VALUE = 0; const MAXIMUM_VALUE = 100; @@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 100; * @param {number} [props.radius=60] - Radius of the gauge circle * @param {number} [props.strokeWidth=15] - Width of the gauge stroke * @param {number} [props.threshold=50] - Threshold for color change + * @param {number} [props.precision=1] - Precision of the progress percentage + * @param {string} [props.unit="%"] - Unit of progress * * @example * { +const CustomGauge = ({ + isLoading = false, + progress = 0, + radius = 70, + strokeWidth = 15, + threshold = 50, + precision = 1, + unit = "%", +}) => { const theme = useTheme(); // Calculate the length of the stroke for the circle const { circumference, totalSize, strokeLength } = useMemo( @@ -57,6 +69,20 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = ? theme.palette.error.lowContrast : theme.palette.accent.main; + if (isLoading) { + return ( + + + + ); + } + return ( - {`${progressWithinRange.toFixed(1)}%`} + {`${progressWithinRange.toFixed(precision)}${unit}`} ); @@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = export default CustomGauge; CustomGauge.propTypes = { + isLoading: PropTypes.bool, progress: PropTypes.number, radius: PropTypes.number, strokeWidth: PropTypes.number, threshold: PropTypes.number, + precision: PropTypes.number, + unit: PropTypes.string, }; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx index 21d9a35d8..e9f235112 100644 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -1,12 +1,12 @@ import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; import Gauge from "../../../../../Components/Charts/CustomGauge"; import Typography from "@mui/material/Typography"; // Utils import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getPercentage } from "./utils"; +import { getPercentage } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; const GaugeBox = ({ title, subtitle, children }) => { const theme = useTheme(); @@ -14,6 +14,8 @@ const GaugeBox = ({ title, subtitle, children }) => { {children} @@ -23,52 +25,85 @@ const GaugeBox = ({ title, subtitle, children }) => { ); }; -const Gauges = ({ diagnostics }) => { +GaugeBox.propTypes = { + title: PropTypes.string.isRequired, + subtitle: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +const Gauges = ({ diagnostics, isLoading }) => { const heapTotalSize = getPercentage( - diagnostics?.v8HeapStats?.totalHeapSizeMb, - diagnostics?.v8HeapStats?.heapSizeLimitMb + diagnostics?.v8HeapStats?.totalHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes ); const heapUsedSize = getPercentage( - diagnostics?.v8HeapStats?.usedHeapSizeMb, - diagnostics?.v8HeapStats?.heapSizeLimitMb + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes ); const actualHeapUsed = getPercentage( - diagnostics?.v8HeapStats?.usedHeapSizeMb, - diagnostics?.v8HeapStats?.totalHeapSizeMb + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.totalHeapSizeBytes ); + const theme = useTheme(); + const { t } = useTranslation(); return ( - + - + - + + + + ); }; Gauges.propTypes = { - diagnostics: PropTypes.object.isRequired, + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, }; export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js deleted file mode 100644 index bdc570a3b..000000000 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js +++ /dev/null @@ -1,4 +0,0 @@ -export const getPercentage = (value, total) => { - if (!value || !total) return 0; - return (value / total) * 100; -}; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx new file mode 100644 index 000000000..103d1c698 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -0,0 +1,97 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; + +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; +import { formatBytes } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; + +const StatsCard = ({ title, value, unit = "", isLoading }) => { + const theme = useTheme(); + return ( + + {isLoading ? ( + + + + ) : ( + + + {title} + + + {value} {unit} + + + )} + + ); +}; + +StatsCard.propTypes = { + title: PropTypes.string, + value: PropTypes.string, + unit: PropTypes.string, + isLoading: PropTypes.bool, +}; + +const Stats = ({ diagnostics, isLoading }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + + + + + + + + + ); +}; + +Stats.propTypes = { + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, +}; + +export default Stats; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx index 9d7ee4639..e000c05bc 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -2,6 +2,8 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Gauges from "./components/gauges"; +import Stats from "./components/stats"; +import Divider from "@mui/material/Divider"; import { useTheme } from "@emotion/react"; import { useState } from "react"; @@ -15,14 +17,26 @@ const Diagnostics = () => { const theme = useTheme(); const { t } = useTranslation(); const [diagnostics, isLoading, error] = useFetchDiagnostics(); - console.log(diagnostics); // Setup return ( - {t("diagnosticsPage.description")} + {t("diagnosticsPage.diagnosticDescription")} - + + + + + ); }; diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js new file mode 100644 index 000000000..a3b75631f --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/utils/utils.js @@ -0,0 +1,16 @@ +export const getPercentage = (value, total) => { + if (!value || !total) return 0; + return (value / total) * 100; +}; + +export const formatBytes = (bytes) => { + if (!bytes) return "N/A"; + + if (bytes === 0) return "0 Bytes"; + + if (bytes >= 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } + + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +}; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index d8bd323a3..b65f9c0fd 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Select from "../../../Components/Inputs/Select"; import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; import { useFetchLogs } from "../../../Hooks/logHooks"; import { useTheme } from "@emotion/react"; @@ -53,10 +54,13 @@ const Logs = () => { {t("logsPage.description")} + + {t("logsPage.logLevelSelect.title")} tz._id === form.timezone) || null} + inputValue={timezoneValue} + handleInputChange={(newVal) => setTimezoneValue(newVal)} + handleChange={(newValue) => { + setTimezoneValue(""); + handleFormChange({ + target: { name: "timezone", value: newValue?._id || "" }, + }); + }} + isAdorned={false} /> From a7e26d553ff0d1a41aa6f37f0dc64b130c5c6bac Mon Sep 17 00:00:00 2001 From: Yashaswee Kesharwani Date: Sat, 12 Jul 2025 23:22:42 +0530 Subject: [PATCH 07/27] chore: format code using npm run format --- client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index 5ed9bb122..de47cfc78 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -16,7 +16,6 @@ import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useState } from "react"; - const TabSettings = ({ isCreate, tabValue, From 39d666df6b51c7c779adc1f27b0af794ba8d4f92 Mon Sep 17 00:00:00 2001 From: shanika Jayawardane Date: Sat, 12 Jul 2025 15:13:24 -0600 Subject: [PATCH 08/27] add the checkboxes for dropdown list and remove searchbox list inside it --- .../Maintenance/CreateMaintenance/index.jsx | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index 0be00e8da..19cb5b16d 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -222,23 +222,14 @@ const CreateMaintenance = () => { }; const handleMonitorsChange = (selected) => { - if (selected.some((m) => m._id === "__all__")) { - if (selectedMonitors.length !== monitors.length) { - setSelectedMonitors(monitors); - } else { - setSelectedMonitors([]); - } - } else { - // Ensure all are objects - const selectedObjs = selected - .map((sel) => - typeof sel === "string" ? monitors.find((m) => m._id === sel) : sel - ) - .filter(Boolean); - setSelectedMonitors(selectedObjs); - } + // Ensure all are objects + const selectedObjs = selected + .map((sel) => (typeof sel === "string" ? monitors.find((m) => m._id === sel) : sel)) + .filter(Boolean); + setSelectedMonitors(selectedObjs); + const { error } = maintenanceWindowValidation.validate( - { monitors: selected }, + { monitors: selectedObjs }, { abortEarly: false } ); setErrors((prev) => { @@ -246,8 +237,6 @@ const CreateMaintenance = () => { }); }; - const allSelected = selectedMonitors.length === monitors.length && monitors.length > 0; - const handleSubmit = async () => { if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return; // Build timestamp for maintenance window from startDate and startTime @@ -303,10 +292,6 @@ const CreateMaintenance = () => { } }; - // Add Select All option to the dropdown - const selectAllOption = { _id: "__all__", name: t("selectAll") }; - const monitorOptions = [selectAllOption, ...monitors]; - return ( { id={"monitors"} label={t("addMonitors")} multiple={true} - isAdorned={false} - options={monitorOptions} + isAdorned={true} + options={monitors} filteredBy="name" secondaryLabel={"type"} inputValue={search} From 2bb78181163b4d445895632d8a3beb92827ecc42 Mon Sep 17 00:00:00 2001 From: Yashaswee Kesharwani Date: Sun, 13 Jul 2025 03:52:16 +0530 Subject: [PATCH 09/27] fix: replaced remaining Selects with Search in Settings Tab - optimised with useMemo and standardised the no unit found --- client/src/Components/Inputs/Search/index.jsx | 9 ++++- .../src/Pages/Settings/SettingsTimeZone.jsx | 40 +++++++++++++++---- .../Create/Components/Tabs/Settings.jsx | 36 ++++++++++++----- client/src/locales/en.json | 4 +- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/Inputs/Search/index.jsx index 4624b2cba..f536bd5b5 100644 --- a/client/src/Components/Inputs/Search/index.jsx +++ b/client/src/Components/Inputs/Search/index.jsx @@ -24,6 +24,7 @@ import { useTranslation } from "react-i18next"; * @param {Function} props.handleChange - Function to call when the input changes * @param {Function} Prop.onBlur - Function to call when the input is blured * @param {Object} props.sx - Additional styles to apply to the component + * @param {string} props.unit - Label to identify type of options * @returns {JSX.Element} The rendered Search component */ @@ -68,6 +69,7 @@ const Search = ({ startAdornment, endAdornment, onBlur, + unit = "option", }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -186,7 +188,12 @@ const Search = ({ ); if (filtered.length === 0) { - return [{ [filteredBy]: "No monitors found", noOptions: true }]; + return [ + { + [filteredBy]: t("general.noOptionsFound", { unit: unit }), + noOptions: true, + }, + ]; } return filtered; }} diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx index 346e4e546..4b17f3f81 100644 --- a/client/src/Pages/Settings/SettingsTimeZone.jsx +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -2,16 +2,37 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import ConfigBox from "../../Components/ConfigBox"; -import Select from "../../Components/Inputs/Select"; +import Search from "../../Components/Inputs/Search"; import timezones from "../../Utils/timezones.json"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; import { useTranslation } from "react-i18next"; +import { useCallback, useMemo, useState } from "react"; const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { const theme = useTheme(); const { t } = useTranslation(); + const [rawInput, setRawInput] = useState(""); + + const selectedTimezone = useMemo( + () => timezones.find((tz) => tz._id === timezone) ?? null, + [timezone, timezones] + ); + + const handleTimezoneChange = useCallback( + (newValue) => { + setRawInput(""); + handleChange({ + target: { + name: "timezone", + value: newValue?._id ?? "", + }, + }); + }, + [handleChange] + ); + return ( @@ -28,12 +49,17 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { -