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;