From 460a597e12cdb465c2b3464d324bd94a6c62ca64 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 31 Jan 2025 13:43:57 -0800 Subject: [PATCH] Refactor infrastructure details --- .../Components/CustomThreshold/index.jsx | 2 +- .../AreaChartBoxes/InfraAreaChart.jsx | 37 + .../Components/AreaChartBoxes/index.jsx | 138 ++++ .../Components/AreaChartBoxes/skeleton.jsx | 25 + .../Components/BaseContainer/index.jsx | 43 + .../Details/Components/GaugeBoxes/Gauge.jsx | 62 ++ .../Details/Components/GaugeBoxes/index.jsx | 80 ++ .../Components/GaugeBoxes/skeleton.jsx | 26 + .../Details/Components/StatusBoxes/index.jsx | 109 +++ .../Hooks/useHardwareMonitorsFetch.jsx | 38 + .../Details/Hooks/useHardwareUtils.jsx | 137 ++++ .../Pages/Infrastructure/Details/empty.jsx | 36 - .../Pages/Infrastructure/Details/index.jsx | 753 ++---------------- .../Pages/Infrastructure/Monitors/index.jsx | 1 - Client/src/Pages/Infrastructure/skeleton.jsx | 73 -- 15 files changed, 782 insertions(+), 778 deletions(-) create mode 100644 Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx create mode 100644 Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx delete mode 100644 Client/src/Pages/Infrastructure/Details/empty.jsx delete mode 100644 Client/src/Pages/Infrastructure/skeleton.jsx diff --git a/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx index 922cb46d8..d91e899c5 100644 --- a/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ b/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -117,7 +117,7 @@ CustomThreshold.propTypes = { fieldName: PropTypes.string, fieldValue: PropTypes.string.isRequired, onFieldChange: PropTypes.func.isRequired, - onFieldBlur: PropTypes.func.isRequired, + onFieldBlur: PropTypes.func, alertUnit: PropTypes.string.isRequired, infrastructureMonitor: PropTypes.object.isRequired, errors: PropTypes.object.isRequired, diff --git a/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx new file mode 100644 index 000000000..9544bcf2a --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx @@ -0,0 +1,37 @@ +// Components +import { Typography } from "@mui/material"; +import BaseContainer from "../BaseContainer"; +import AreaChart from "../../../../../Components/Charts/AreaChart"; +// Utils +import { useTheme } from "@emotion/react"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +const InfraAreaChart = ({ config }) => { + const theme = useTheme(); + const { getDimensions } = useHardwareUtils(); + return ( + + + {config.heading} + + + + ); +}; + +export default InfraAreaChart; diff --git a/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx new file mode 100644 index 000000000..4052b11b5 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx @@ -0,0 +1,138 @@ +// Components +import { Stack } from "@mui/material"; +import InfraAreaChart from "./InfraAreaChart"; +import SkeletonLayout from "./skeleton"; + +// Utils +import { + PercentTick, + TzTick, + InfrastructureTooltip, + TemperatureTooltip, +} from "../../../../../Components/Charts/Utils/chartUtils"; +import { useTheme } from "@emotion/react"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => { + const theme = useTheme(); + const { buildTemps } = useHardwareUtils(); + + if (!shouldRender) { + return ; + } + + const { stats } = monitor ?? {}; + const { checks } = stats; + + let latestCheck = checks[0]; + const { temps, tempKeys } = buildTemps(checks); + + const configs = [ + { + type: "memory", + data: checks, + dataKeys: ["avgMemoryUsage"], + heading: "Memory usage", + strokeColor: theme.palette.accent.main, // CAIO_REVIEW + gradientStartColor: theme.palette.accent.main, // CAIO_REVIEW + yLabel: "Memory usage", + yDomain: [0, 1], + yTick: , + xTick: , + toolTip: ( + + ), + }, + { + type: "cpu", + data: checks, + dataKeys: ["avgCpuUsage"], + heading: "CPU usage", + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yLabel: "CPU usage", + yDomain: [0, 1], + yTick: , + xTick: , + toolTip: ( + + ), + }, + { + type: "temperature", + data: temps, + dataKeys: tempKeys, + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + heading: "CPU Temperature", + yLabel: "Temperature", + xTick: , + yDomain: [ + 0, + Math.max(Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, 200), + ], + toolTip: ( + + ), + }, + ...(latestCheck?.disks?.map((disk, idx) => ({ + type: "disk", + data: checks, + diskIndex: idx, + dataKeys: [`disks[${idx}].usagePercent`], + heading: `Disk${idx} usage`, + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yLabel: "Disk Usage", + yDomain: [0, 1], + yTick: , + xTick: , + toolTip: ( + + ), + })) || []), + ]; + + return ( + *": { + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, + }, + }} + > + {configs.map((config) => ( + + ))} + + ); +}; + +export default AreaChartBoxes; diff --git a/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx new file mode 100644 index 000000000..f787b88ec --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx @@ -0,0 +1,25 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx new file mode 100644 index 000000000..246769751 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx @@ -0,0 +1,43 @@ +/** + * 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 + */ + +// Components +import { Box } from "@mui/material"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import PropTypes from "prop-types"; + +const BaseContainer = ({ children, sx = {} }) => { + const theme = useTheme(); + const { getDimensions } = useHardwareUtils(); + return ( + + {children} + + ); +}; + +BaseContainer.propTypes = { + children: PropTypes.node.isRequired, + sx: PropTypes.object, +}; + +export default BaseContainer; diff --git a/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx new file mode 100644 index 000000000..b1202682d --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx @@ -0,0 +1,62 @@ +// Components +import CustomGauge from "../../../../../Components/Charts/CustomGauge"; +import BaseContainer from "../BaseContainer"; +import { Stack, Typography, Box } from "@mui/material"; +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { + const theme = useTheme(); + + return ( + + + + {heading} + + + {metricOne} + {valueOne} + + + {metricTwo} + {valueTwo} + + + + + ); +}; + +Gauge.propTypes = { + value: PropTypes.number, + heading: PropTypes.string, + metricOne: PropTypes.string, + valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + metricTwo: PropTypes.string, + valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), +}; + +export default Gauge; diff --git a/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx new file mode 100644 index 000000000..1d2e044ce --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx @@ -0,0 +1,80 @@ +// Components +import { Stack } from "@mui/material"; +import Gauge from "./Gauge"; +import SkeletonLayout from "./skeleton"; + +// Utils +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useTheme } from "@emotion/react"; + +const Gauges = ({ shouldRender, monitor }) => { + const { decimalToPercentage, formatBytes } = useHardwareUtils(); + const theme = useTheme(); + + if (!shouldRender) { + return ; + } + + const { stats } = monitor ?? {}; + let latestCheck = stats?.aggregateData?.latestCheck; + const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0; + const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0; + const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; + const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0; + const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0; + const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; + + const gauges = [ + { + type: "memory", + value: decimalToPercentage(memoryUsagePercent), + heading: "Memory usage", + metricOne: "Used", + valueOne: formatBytes(memoryUsedBytes, true), + metricTwo: "Total", + valueTwo: formatBytes(memoryTotalBytes, true), + }, + { + type: "cpu", + value: decimalToPercentage(cpuUsagePercent), + heading: "CPU usage", + metricOne: "Cores", + valueOne: cpuPhysicalCores ?? 0, + metricTwo: "Frequency", + valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`, + }, + ...(latestCheck?.disk ?? []).map((disk, idx) => ({ + type: "disk", + diskIndex: idx, + value: decimalToPercentage(disk.usage_percent), + heading: `Disk${idx} usage`, + metricOne: "Used", + valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true), + metricTwo: "Total", + valueTwo: formatBytes(disk.total_bytes, true), + })), + ]; + + return ( + + {gauges.map((gauge) => { + return ( + + ); + })} + + ); +}; + +export default Gauges; diff --git a/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx new file mode 100644 index 000000000..347a52901 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx @@ -0,0 +1,26 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + + {Array.from({ length: 3 }).map((_, idx) => { + return ( + + ); + })} + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..aa7ab97ae --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -0,0 +1,109 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; + +//Utils +import useUtils from "../../../../../Pages/Uptime/Monitors/Hooks/useUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; + +const InfraStatBoxes = ({ shouldRender, monitor }) => { + // Utils + const { formatBytes } = useHardwareUtils(); + const { statusStyles, determineState } = useUtils(); + + const { stats, uptimePercentage } = monitor ?? {}; + const latestCheck = stats?.aggregateData?.latestCheck; + + // Get data from latest check + const physicalCores = latestCheck?.cpu?.physical_core ?? 0; + const logicalCores = latestCheck?.cpu?.logical_core ?? 0; + const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; + const cpuTemperature = + latestCheck?.cpu?.temperature?.length > 0 + ? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) / + latestCheck.cpu.temperature.length + : 0; + const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; + const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0; + const os = latestCheck?.host?.os ?? undefined; + const platform = latestCheck?.host?.platform ?? undefined; + const osPlatform = + typeof os === "undefined" && typeof platform === "undefined" + ? undefined + : `${os} ${platform}`; + + return ( + + + + {physicalCores} + cores + + } + /> + + {logicalCores} + cores + + } + /> + + {(cpuFrequency / 1000).toFixed(2)} + Ghz + + } + /> + + {cpuTemperature.toFixed(2)} + C + + } + /> + + + + {(uptimePercentage * 100).toFixed(2)} + % + + } + /> + + + ); +}; + +export default InfraStatBoxes; diff --git a/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx new file mode 100644 index 000000000..49972debb --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { networkService } from "../../../../main"; + +const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [monitor, setMonitor] = useState(undefined); + + const { authToken } = useSelector((state) => state.auth); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await networkService.getHardwareDetailsByMonitorId({ + authToken: authToken, + monitorId: monitorId, + dateRange: dateRange, + }); + response.data.data; + setMonitor(response.data.data); + } catch (error) { + setNetworkError(true); + } finally { + setIsLoading(false); + } + }; + fetchData(); + }, [monitorId, dateRange, authToken]); + + return { + isLoading, + networkError, + monitor, + }; +}; + +export { useHardwareMonitorsFetch }; diff --git a/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx new file mode 100644 index 000000000..bc540dfe3 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -0,0 +1,137 @@ +import { Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +// Constants +const BASE_BOX_PADDING_VERTICAL = 4; +const BASE_BOX_PADDING_HORIZONTAL = 8; +const TYPOGRAPHY_PADDING = 8; +const CHART_CONTAINER_HEIGHT = 300; + +const useHardwareUtils = () => { + const theme = useTheme(); + + const getDimensions = () => { + const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2; + const totalChartContainerPadding = + parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2; + return { + baseBoxPaddingVertical: BASE_BOX_PADDING_VERTICAL, + baseBoxPaddingHorizontal: BASE_BOX_PADDING_HORIZONTAL, + totalContainerPadding: parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2, + areaChartHeight: + CHART_CONTAINER_HEIGHT - totalChartContainerPadding - totalTypographyPadding, + }; + }; + + const formatBytes = (bytes, space = false) => { + if (bytes === undefined || bytes === null) + return ( + <> + {0} + {space ? " " : ""} + GB + + ); + if (typeof bytes !== "number") + return ( + <> + {0} + {space ? " " : ""} + GB + + ); + if (bytes === 0) + return ( + <> + {0} + {space ? " " : ""} + GB + + ); + + const GB = bytes / (1024 * 1024 * 1024); + const MB = bytes / (1024 * 1024); + + if (GB >= 1) { + return ( + <> + {Number(GB.toFixed(0))} + {space ? " " : ""} + GB + + ); + } else { + return ( + <> + {Number(MB.toFixed(0))} + MB + + ); + } + }; + + /** + * Converts a decimal value to a percentage + * + * @function decimalToPercentage + * @param {number} value - Decimal value to convert + * @returns {number} Percentage representation + * + * @example + * decimalToPercentage(0.75) // Returns 75 + * decimalToPercentage(null) // Returns 0 + */ + const decimalToPercentage = (value) => { + if (value === null || value === undefined) return 0; + return value * 100; + }; + + const buildTemps = (checks) => { + let numCores = 1; + if (checks === null) return { temps: [], tempKeys: [] }; + + for (const check of checks) { + if (check?.avgTemperature?.length > numCores) { + numCores = check.avgTemperature.length; + break; + } + } + const temps = checks.map((check) => { + // If there's no data, set the temperature to 0 + if ( + check?.avgTemperature?.length === 0 || + check?.avgTemperature === undefined || + check?.avgTemperature === null + ) { + check.avgTemperature = Array(numCores).fill(0); + } + const res = check?.avgTemperature?.reduce( + (acc, cur, idx) => { + acc[`core${idx + 1}`] = cur; + return acc; + }, + { + _id: check._id, + } + ); + return res; + }); + if (temps.length === 0 || !temps[0]) { + return { temps: [], tempKeys: [] }; + } + + return { + tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"), + temps, + }; + }; + + return { + formatBytes, + decimalToPercentage, + buildTemps, + getDimensions, + }; +}; + +export { useHardwareUtils }; diff --git a/Client/src/Pages/Infrastructure/Details/empty.jsx b/Client/src/Pages/Infrastructure/Details/empty.jsx deleted file mode 100644 index ee3cdb4df..000000000 --- a/Client/src/Pages/Infrastructure/Details/empty.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTheme } from "@emotion/react"; -import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; -import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; -import { Box, Typography, Stack } from "@mui/material"; -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -const Empty = ({ styles }) => { - const theme = useTheme(); - const mode = useSelector((state) => state.ui.mode); - return ( - - - {mode === "light" ? : } - - Your infrastructure dashboard will show here - - Hang tight! Data is loading - - - - ); -}; - -Empty.propTypes = { - styles: PropTypes.object, - mode: PropTypes.string, -}; - -export default Empty; diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index 9d6b09330..8e566c664 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -1,681 +1,100 @@ -import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +// Components +import { Stack, Typography } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { Button, ButtonGroup, Stack, Box, Typography } from "@mui/material"; +import MonitorStatusHeader from "../../../Components/MonitorStatusHeader"; +import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; +import StatusBoxes from "./Components/StatusBoxes"; +import GaugeBoxes from "./Components/GaugeBoxes"; +import AreaChartBoxes from "./Components/AreaChartBoxes"; +import GenericFallback from "../../../Components/GenericFallback"; + +// Utils import { useTheme } from "@emotion/react"; -import CustomGauge from "../../../Components/Charts/CustomGauge"; -import AreaChart from "../../../Components/Charts/AreaChart"; -import { useSelector } from "react-redux"; -import { networkService } from "../../../main"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import useUtils from "../../Uptime/Monitors/Hooks/useUtils"; -import { useNavigate } from "react-router-dom"; -import Empty from "./empty"; -import { logger } from "../../../Utils/Logger"; -import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils"; -import { TzTick, PercentTick } from "../../../Components/Charts/Utils/chartUtils"; -import { - InfrastructureTooltip, - TemperatureTooltip, -} from "../../../Components/Charts/Utils/chartUtils"; -import PropTypes from "prop-types"; -import StatBox from "../../../Components/StatBox"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useHardwareMonitorsFetch } from "./Hooks/useHardwareMonitorsFetch"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; -const BASE_BOX_PADDING_VERTICAL = 4; -const BASE_BOX_PADDING_HORIZONTAL = 8; -const TYPOGRAPHY_PADDING = 8; -/** - * Converts bytes to gigabytes - * @param {number} bytes - Number of bytes to convert - * @returns {number} Converted value in gigabytes - */ -const formatBytes = (bytes, space = false) => { - if (bytes === undefined || bytes === null) - return ( - <> - {0} - {space ? " " : ""} - GB - - ); - if (typeof bytes !== "number") - return ( - <> - {0} - {space ? " " : ""} - GB - - ); - if (bytes === 0) - return ( - <> - {0} - {space ? " " : ""} - GB - - ); +// Constants +const BREADCRUMBS = [ + { name: "infrastructure monitors", path: "/infrastructure" }, + { name: "details", path: "" }, +]; +const InfrastructureDetails = () => { + // Redux state - const GB = bytes / (1024 * 1024 * 1024); - const MB = bytes / (1024 * 1024); + // Local state + const [dateRange, setDateRange] = useState("day"); - if (GB >= 1) { + // Utils + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const { monitorId } = useParams(); + + const { isLoading, networkError, monitor } = useHardwareMonitorsFetch({ + monitorId, + dateRange, + }); + + if (networkError === true) { return ( - <> - {Number(GB.toFixed(0))} - {space ? " " : ""} - GB - - ); - } else { - return ( - <> - {Number(MB.toFixed(0))} - MB - + + + Network error + + Please check your connection + ); } -}; -/** - * Converts a decimal value to a percentage - * - * @function decimalToPercentage - * @param {number} value - Decimal value to convert - * @returns {number} Percentage representation - * - * @example - * decimalToPercentage(0.75) // Returns 75 - * decimalToPercentage(null) // Returns 0 - */ -const decimalToPercentage = (value) => { - if (value === null || value === undefined) return 0; - return value * 100; -}; - -/** - * 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, sx = {} }) => { - const theme = useTheme(); - return ( - - {children} - - ); -}; - -BaseBox.propTypes = { - children: PropTypes.node.isRequired, - sx: PropTypes.object, -}; - -/** - * 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(); + // if (!isLoading && monitor?.stats?.checks?.length === 0) { + // return ( + // + // + // + // + // No data found for this monitor + // + // + // ); + // } return ( - - - - {heading} - - - {metricOne} - {valueOne} - - - {metricTwo} - {valueTwo} - - - - - ); -}; - -GaugeBox.propTypes = { - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - heading: PropTypes.string.isRequired, - metricOne: PropTypes.string.isRequired, - valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]) - .isRequired, - metricTwo: PropTypes.string.isRequired, - valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]) - .isRequired, -}; - -/** - * Renders the infrastructure details page - * @returns {React.ReactElement} Infrastructure details page component - */ -const InfrastructureDetails = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const { monitorId } = useParams(); - const navList = [ - { name: "infrastructure monitors", path: "/infrastructure" }, - { name: "details", path: `/infrastructure/${monitorId}` }, - ]; - const [monitor, setMonitor] = useState(null); - const { authToken } = useSelector((state) => state.auth); - const [dateRange, setDateRange] = useState("day"); - const { statusColor, statusStyles, determineState } = useUtils(); - // 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 - - const buildStatBoxes = (stats, uptime) => { - if (Object.keys(stats).length === 0) return []; - let latestCheck = stats?.aggregateData?.latestCheck ?? null; - if (latestCheck === null) return []; - - // Extract values from latest check - const physicalCores = latestCheck?.cpu?.physical_core ?? 0; - const logicalCores = latestCheck?.cpu?.logical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - const cpuTemperature = - latestCheck?.cpu?.temperature?.length > 0 - ? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) / - latestCheck.cpu.temperature.length - : 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0; - const os = latestCheck?.host?.os ?? null; - const platform = latestCheck?.host?.platform ?? null; - const osPlatform = os === null && platform === null ? null : `${os} ${platform}`; - return [ - { - id: 7, - sx: statusStyles[determineState(monitor)], - heading: "Status", - subHeading: monitor?.status === true ? "Active" : "Inactive", - }, - { - id: 0, - heading: "CPU (Physical)", - subHeading: ( - <> - {physicalCores} - cores - - ), - }, - { - id: 1, - heading: "CPU (Logical)", - subHeading: ( - <> - {logicalCores} - cores - - ), - }, - { - id: 2, - heading: "CPU Frequency", - subHeading: ( - <> - {(cpuFrequency / 1000).toFixed(2)} - Ghz - - ), - }, - { - id: 3, - heading: "Average CPU Temperature", - subHeading: ( - <> - {cpuTemperature.toFixed(2)} - C - - ), - }, - { - id: 4, - heading: "Memory", - subHeading: formatBytes(memoryTotalBytes), - }, - { - id: 5, - heading: "Disk", - subHeading: formatBytes(diskTotalBytes), - }, - { - id: 6, - heading: "Uptime", - subHeading: ( - <> - {(uptime * 100).toFixed(2)} - % - - ), - }, - - { - id: 8, - heading: "OS", - subHeading: osPlatform, - }, - ]; - }; - - const buildGaugeBoxConfigs = (stats) => { - if (Object.keys(stats).length === 0) return []; - - let latestCheck = stats?.aggregateData?.latestCheck ?? null; - if (latestCheck === null) return []; - - // Extract values from latest check - const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0; - const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0; - const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - return [ - { - type: "memory", - value: decimalToPercentage(memoryUsagePercent), - heading: "Memory usage", - metricOne: "Used", - valueOne: formatBytes(memoryUsedBytes, true), - metricTwo: "Total", - valueTwo: formatBytes(memoryTotalBytes, true), - }, - { - type: "cpu", - value: decimalToPercentage(cpuUsagePercent), - heading: "CPU usage", - metricOne: "Cores", - valueOne: cpuPhysicalCores ?? 0, - metricTwo: "Frequency", - valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`, - }, - ...(latestCheck?.disk ?? []).map((disk, idx) => ({ - type: "disk", - diskIndex: idx, - value: decimalToPercentage(disk.usage_percent), - heading: `Disk${idx} usage`, - metricOne: "Used", - valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true), - metricTwo: "Total", - valueTwo: formatBytes(disk.total_bytes, true), - })), - ]; - }; - - const buildTemps = (checks) => { - let numCores = 1; - if (checks === null) return { temps: [], tempKeys: [] }; - - for (const check of checks) { - if (check?.avgTemperature?.length > numCores) { - numCores = check.avgTemperature.length; - break; - } - } - const temps = checks.map((check) => { - // If there's no data, set the temperature to 0 - if ( - check?.avgTemperature?.length === 0 || - check?.avgTemperature === undefined || - check?.avgTemperature === null - ) { - check.avgTemperature = Array(numCores).fill(0); - } - const res = check?.avgTemperature?.reduce( - (acc, cur, idx) => { - acc[`core${idx + 1}`] = cur; - return acc; - }, - { - _id: check._id, - } - ); - return res; - }); - if (temps.length === 0 || !temps[0]) { - return { temps: [], tempKeys: [] }; - } - - return { - tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"), - temps, - }; - }; - - const buildAreaChartConfigs = (checks) => { - let latestCheck = checks[0] ?? null; - if (latestCheck === null) return []; - const { temps, tempKeys } = buildTemps(checks); - return [ - { - type: "memory", - data: checks, - dataKeys: ["avgMemoryUsage"], - heading: "Memory usage", - strokeColor: theme.palette.accent.main, // CAIO_REVIEW - gradientStartColor: theme.palette.accent.main, // CAIO_REVIEW - yLabel: "Memory usage", - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - }, - { - type: "cpu", - data: checks, - dataKeys: ["avgCpuUsage"], - heading: "CPU usage", - strokeColor: theme.palette.success.main, - gradientStartColor: theme.palette.success.main, - yLabel: "CPU usage", - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - }, - { - type: "temperature", - data: temps, - dataKeys: tempKeys, - strokeColor: theme.palette.error.main, - gradientStartColor: theme.palette.error.main, - heading: "CPU Temperature", - yLabel: "Temperature", - xTick: , - yDomain: [ - 0, - Math.max( - Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, - 200 - ), - ], - toolTip: ( - - ), - }, - ...(latestCheck?.disks?.map((disk, idx) => ({ - type: "disk", - data: checks, - diskIndex: idx, - dataKeys: [`disks[${idx}].usagePercent`], - heading: `Disk${idx} usage`, - strokeColor: theme.palette.warning.main, - gradientStartColor: theme.palette.warning.main, - yLabel: "Disk Usage", - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - })) || []), - ]; - }; - - // Fetch data - useEffect(() => { - const fetchData = async () => { - try { - const response = await networkService.getHardwareDetailsByMonitorId({ - authToken: authToken, - monitorId: monitorId, - dateRange: dateRange, - }); - response.data.data; - setMonitor(response.data.data); - } catch (error) { - navigate("/not-found", { replace: true }); - logger.error(error); - } - }; - fetchData(); - }, [authToken, monitorId, dateRange, navigate]); - - const statBoxConfigs = buildStatBoxes( - monitor?.stats ?? {}, - monitor?.uptimePercentage ?? "Unknown" - ); - const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.stats ?? {}); - const areaChartConfigs = buildAreaChartConfigs(monitor?.stats?.checks ?? []); - const lastChecked = - Date.now() - new Date(monitor?.stats?.aggregateData?.latestCheck?.createdAt); - return ( - - - {monitor?.stats.checks?.length > 0 ? ( - - - - - - - {monitor.name} - - {monitor.url || "..."} - - - Checking every {formatDurationRounded(monitor?.interval)} - - - Last checked {formatDurationSplit(lastChecked).time}{" "} - {formatDurationSplit(lastChecked).format} ago - - - - - {statBoxConfigs.map((statBox) => ( - - ))} - - - - {gaugeBoxConfigs.map((config) => { - return ( - - ); - })} - - - - Showing statistics for past{" "} - {dateRange === "day" - ? "24 hours" - : dateRange === "week" - ? "7 days" - : "30 days"} - . - - - - - - - - *": { - flexBasis: `calc(50% - ${theme.spacing(8)})`, - maxWidth: `calc(50% - ${theme.spacing(8)})`, - }, - }} - > - {areaChartConfigs.map((config) => { - return ( - - - {config.heading} - - - - ); - })} - - - ) : ( - - )} - + + + + + + + + ); }; diff --git a/Client/src/Pages/Infrastructure/Monitors/index.jsx b/Client/src/Pages/Infrastructure/Monitors/index.jsx index 6418dfd12..00eb48330 100644 --- a/Client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/Client/src/Pages/Infrastructure/Monitors/index.jsx @@ -75,7 +75,6 @@ const InfrastructureMonitors = () => { ); } - console.log(isLoading); return ( diff --git a/Client/src/Pages/Infrastructure/skeleton.jsx b/Client/src/Pages/Infrastructure/skeleton.jsx deleted file mode 100644 index b6d29dc38..000000000 --- a/Client/src/Pages/Infrastructure/skeleton.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout;