From cb392eaaba8d9f9eb815c7d8e50e92d4940d8e0d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 28 Jan 2026 23:26:00 +0000 Subject: [PATCH] gauges, charts --- .../charts/HistogramInfrastructure.tsx | 201 ++++++++++++++++++ client/src/Components/v2/monitors/index.tsx | 1 + .../Details/Components/Charts.tsx | 98 +++++++++ .../Details/Components/Gauges.tsx | 110 ++++++++++ .../Details/Components/TabOverview.tsx | 28 ++- .../Pages/Infrastructure/Details/index.tsx | 9 +- client/src/Types/Check.ts | 12 +- client/src/Types/Monitor.ts | 78 ++++--- client/src/Utils/InfraUtils.ts | 1 - client/src/locales/en.json | 25 +++ server/src/db/models/Check.ts | 13 +- .../checks/MongoChecksRepistory.ts | 15 +- server/src/types/check.ts | 13 +- server/src/types/monitor.ts | 78 ++++--- 14 files changed, 596 insertions(+), 86 deletions(-) create mode 100644 client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx create mode 100644 client/src/Pages/Infrastructure/Details/Components/Charts.tsx create mode 100644 client/src/Pages/Infrastructure/Details/Components/Gauges.tsx diff --git a/client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx b/client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx new file mode 100644 index 000000000..86b6c3d47 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramInfrastructure.tsx @@ -0,0 +1,201 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; +import { Fragment, useId } from "react"; +import { XTick } from "@/Components/v2/monitors"; + +import { useTheme } from "@mui/material/styles"; +import type { HardwareCheckStats } from "@/Types/Monitor"; + +const AREA_COLORS = [ + // Blues + "#3182bd", // Deep blue + "#6baed6", // Medium blue + "#9ecae1", // Light blue + + // Greens + "#74c476", // Soft green + "#a1d99b", // Light green + "#c7e9c0", // Pale green + + // Oranges + "#fdae6b", // Warm orange + "#fdd0a2", // Light orange + "#feedde", // Pale orange + + // Purples + "#9467bd", // Lavender + "#a55194", // Deep magenta + "#c994c7", // Soft magenta + + // Reds + "#ff9896", // Soft red + "#de2d26", // Deep red + "#fc9272", // Medium red + + // Cyans/Teals + "#17becf", // Cyan + "#7fcdbb", // Teal + "#a1dab4", // Light teal + + // Yellows + "#fec44f", // Mustard + "#fee391", // Light yellow + "#ffffd4", // Pale yellow + + // Additional colors + "#e377c2", // Soft pink + "#bcbd22", // Olive + "#2ca02c", // Vibrant green +]; + +const createGradient = ({ + id, + startColor, + endColor, + startOpacity = 0.8, + endOpacity = 0, + direction = "vertical", +}: { + id: string; + startColor: string; + endColor: string; + startOpacity?: number; + endOpacity?: number; + direction?: "vertical" | "horizontal"; +}) => ( + + + + + + +); + +export const HistogramInfrastructure = ({ + range, + title, + type, + idx: _idx, + checks, + xKey, + yDomain, + dataKeys, + gradient = false, + gradientDirection = "vertical", + gradientStartColor, + gradientEndColor, + strokeColor, + fillColor, +}: { + range: string; + title: string; + type: string; + idx: number | null; + checks: HardwareCheckStats[]; + xKey: string; + yDomain?: number[]; + dataKeys: string[]; + gradient?: boolean; + gradientDirection?: "vertical" | "horizontal"; + gradientStartColor?: string; + gradientEndColor?: string; + strokeColor: string; + fillColor?: string; +}) => { + const theme = useTheme(); + const uniqueId = useId(); + let data = checks.reverse(); + + let avgTemps: { bucketDate: string; avg_temp: number | null }[] = []; + let tempYDomain: number[] = []; + if (type === "temp") { + avgTemps = data.map((check) => { + const temps = check.avgTemperature || []; + if (temps.length === 0) return { bucketDate: check._id, avg_temp: null }; + const totalTemp = temps.reduce((sum, temp) => sum + (temp || 0), 0); + const avgTemp = totalTemp / temps.length; + return { bucketDate: check?._id, avg_temp: avgTemp }; + }); + + const maxTemp: number = avgTemps.reduce((max, item) => { + return item.avg_temp && item.avg_temp > max ? item.avg_temp : max; + }, 0); + + tempYDomain = [0, Math.ceil((maxTemp * 1.3) / 10) * 10]; + } + + return ( + + + + ( + + )} + /> + + + + {dataKeys?.map((dataKey, index) => { + const gradientId = `gradient-${uniqueId}-${index}`; + return ( + + {gradient === true && + createGradient({ + id: gradientId, + startColor: gradientStartColor || AREA_COLORS[index], + endColor: gradientEndColor || "transparent", + direction: gradientDirection, + })} + + + ); + })} + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index 4efaae65c..ce90f1b8c 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -9,3 +9,4 @@ export * from "./charts/PiePageSpeed"; export * from "./charts/PiePageSpeedLegend"; export * from "./charts/HistogramPageSpeedDetails"; export * from "./charts/HistogramPageSpeedDetailsTooltip"; +export * from "./charts/HistogramInfrastructure"; diff --git a/client/src/Pages/Infrastructure/Details/Components/Charts.tsx b/client/src/Pages/Infrastructure/Details/Components/Charts.tsx new file mode 100644 index 000000000..cdeba0a77 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/Charts.tsx @@ -0,0 +1,98 @@ +import Grid from "@mui/material/Grid"; +import { HistogramInfrastructure } from "@/Components/v2/monitors"; + +import { useTranslation } from "react-i18next"; +import type { HardwareCheckStats } from "@/Types/Monitor"; +import { useMemo } from "react"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const getChartConfigs = (theme: any, checks: HardwareCheckStats[], t: any) => { + return [ + { + title: t("pages.infrastructure.charts.labels.memory"), + type: "memory", + dataKeys: ["avgMemoryUsage"], + strokeColor: theme.palette.primary.main, + gradientStartColor: theme.palette.primary.main, + yDomain: [0, 1], + idx: null, + }, + { + title: t("pages.infrastructure.charts.labels.cpu"), + type: "cpu", + dataKeys: ["avgCpuUsage"], + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yDomain: [0, 1], + idx: null, + }, + { + title: t("pages.infrastructure.charts.labels.temp"), + type: "temp", + dataKeys: ["avg_temp"], + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + yDomain: [0, 150], + idx: null, + }, + ...(checks[0]?.disks?.map((_, idx) => ({ + title: t("pages.infrastructure.charts.labels.disk", { idx }), + type: "disk", + dataKeys: [`disks[${idx}].usagePercent`], + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yDomain: [0, 1], + idx, + })) || []), + ]; +}; + +export const InfraDetailsCharts = ({ + checks, + range, +}: { + checks: HardwareCheckStats[]; + range: string; +}) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation(); + const chartConfigs = useMemo( + () => getChartConfigs(theme, checks, t), + [theme, checks, t] + ); + + console.log(checks); + return ( + + {chartConfigs.map((config) => { + return ( + + + + ); + })} + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx new file mode 100644 index 000000000..5df7ee7ac --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx @@ -0,0 +1,110 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart, Gauge } from "@/Components/v2/design-elements"; + +import { useTranslation } from "react-i18next"; +import { getGbs, getFrequency } from "@/Utils/InfraUtils"; +import { useTheme } from "@mui/material"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import type { CheckSnapshot, GroupedCheck } from "@/Types/Check"; +import type { HardwareCheckStats } from "@/Types/Monitor"; + +const InfraDetailGauge = ({ + title, + progress, + upperLabel, + upperValue, + lowerLabel, + lowerValue, +}: { + title: string; + progress: number; + upperLabel?: string; + upperValue?: string | number; + lowerLabel?: string; + lowerValue?: string | number; +}) => { + const theme = useTheme(); + return ( + + + + + + {upperLabel} + {upperValue} + + + {lowerLabel} + {lowerValue} + + + ); +}; + +export const InfraDetailsGauges = ({ + snapshot, +}: { + snapshot: CheckSnapshot | undefined; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + if (!snapshot) { + return null; + } + + return ( + + + + {snapshot?.disk?.map((disk, idx) => { + return ( + + ); + })} + + ); +}; diff --git a/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx index c4cc6f0f1..7bb79ae6d 100644 --- a/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx +++ b/client/src/Pages/Infrastructure/Details/Components/TabOverview.tsx @@ -1,10 +1,32 @@ +import Stack from "@mui/material/Stack"; +import { InfraDetailsGauges } from "@/Pages/Infrastructure/Details/Components/Gauges"; import { StatusBoxes } from "@/Pages/Infrastructure/Details/Components/StatusBoxes"; +import { InfraDetailsCharts } from "@/Pages/Infrastructure/Details/Components/Charts"; -import type { Monitor } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; +import type { HardwareStats, Monitor } from "@/Types/Monitor"; -export const TabOverview = ({ monitor }: { monitor: Monitor | undefined }) => { +export const TabOverview = ({ + monitor, + stats, +}: { + monitor: Monitor | undefined; + stats: HardwareStats | undefined; +}) => { + const theme = useTheme(); if (!monitor) { return null; } - return ; + + const checks = stats?.checks || []; + return ( + + + + + + ); }; diff --git a/client/src/Pages/Infrastructure/Details/index.tsx b/client/src/Pages/Infrastructure/Details/index.tsx index c24e5a394..5260c5d15 100644 --- a/client/src/Pages/Infrastructure/Details/index.tsx +++ b/client/src/Pages/Infrastructure/Details/index.tsx @@ -1,6 +1,5 @@ import { BasePage, Tab, Tabs } from "@/Components/v2/design-elements"; import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common"; -import Stack from "@mui/material/Stack"; import { MonitorStatBoxes } from "@/Components/v2/monitors"; import { TabNetwork } from "@/Pages/Infrastructure/Details/Components/TabNetwork"; import { TabOverview } from "@/Pages/Infrastructure/Details/Components/TabOverview"; @@ -46,6 +45,7 @@ const InfrastructureDetails = () => { const monitor = monitorDetailsData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; + const stats = monitorDetailsData?.stats; return ( @@ -68,7 +68,12 @@ const InfrastructureDetails = () => { - {selectedTab === 0 && } + {selectedTab === 0 && ( + + )} {selectedTab === 1 && } ); diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 6f10bea3a..8fc4e5664 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -16,6 +16,7 @@ export interface CheckCpuInfo { physical_core?: number; logical_core?: number; frequency?: number; + current_frequency?: number; temperature?: number[]; free_percent?: number; usage_percent?: number; @@ -43,11 +44,18 @@ export interface CheckCaptureInfo { export interface CheckDiskInfo { device?: string; mountpoint?: string; - read_speed_bytes?: number; - write_speed_bytes?: number; total_bytes?: number; free_bytes?: number; + used_bytes?: number; usage_percent?: number; + total_inodes?: number; + free_inodes?: number; + used_inodes?: number; + inodes_usage_percent?: number; + read_bytes?: number; + write_bytes?: number; + read_time?: number; + write_time?: number; } export interface CheckErrorInfo { diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index aaafb9a11..92af7ccd8 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -108,42 +108,50 @@ export interface PageSpeedDetailsResponse { monitorStats: MonitorStats | null; } +export interface HardwareDiskStats { + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; +} + +export interface HardwareNetStats { + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; +} + +export interface HardwareCheckStats { + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: HardwareDiskStats[]; + net: HardwareNetStats[]; +} + +export interface HardwareStats { + aggregateData: { + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: HardwareCheckStats[]; +} + export interface HardwareDetailsResponse { monitor: Monitor; - stats: { - aggregateData: { - totalChecks: number; - }; - upChecks: { - totalChecks: number; - }; - checks: Array<{ - _id: string; - avgCpuUsage: number; - avgMemoryUsage: number; - avgTemperature: number[]; - disks: Array<{ - name: string; - readSpeed: number; - writeSpeed: number; - totalBytes: number; - freeBytes: number; - usagePercent: number; - }>; - net: Array<{ - name: string; - bytesSentPerSecond: number; - deltaBytesRecv: number; - deltaPacketsSent: number; - deltaPacketsRecv: number; - deltaErrIn: number; - deltaErrOut: number; - deltaDropIn: number; - deltaDropOut: number; - deltaFifoIn: number; - deltaFifoOut: number; - }>; - }>; - }; + stats: HardwareStats; monitorStats: MonitorStats | null; } diff --git a/client/src/Utils/InfraUtils.ts b/client/src/Utils/InfraUtils.ts index 9fba65dbc..07212737e 100644 --- a/client/src/Utils/InfraUtils.ts +++ b/client/src/Utils/InfraUtils.ts @@ -13,7 +13,6 @@ export const getCores = (cores: number | undefined) => { }; export const getAvgTemp = (temps: number[]): string => { - console.log(temps); if (!temps || temps.length === 0) return "N/A"; const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length; return `${avgTemp?.toFixed(2)} °C`; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 0ba2f2651..cfea5299a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -299,6 +299,31 @@ "memory": "Memory", "disk": "Disk", "os": "OS" + }, + "gauges": { + "cpu": { + "lowerLabel": "Max frequency", + "title": "CPU usage", + "upperLabel": "Current frequency" + }, + "disk": { + "lowerLabel": "Free", + "title": "Disk {{idx}} usage", + "upperLabel": "Used" + }, + "memory": { + "lowerLabel": "Free", + "title": "Memory usage", + "upperLabel": "Used" + } + }, + "charts": { + "labels": { + "memory": "Memory usage", + "cpu": "CPU usage", + "temp": "Temp", + "disk": "Disk usage" + } } } }, diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 4d9c499de..7bd1cec6f 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -71,6 +71,7 @@ const cpuSchema = new Schema( physical_core: { type: Number, default: 0 }, logical_core: { type: Number, default: 0 }, frequency: { type: Number, default: 0 }, + current_frequency: { type: Number, default: 0 }, temperature: { type: [Number], default: [] }, free_percent: { type: Number, default: 0 }, usage_percent: { type: Number, default: 0 }, @@ -92,11 +93,18 @@ const diskSchema = new Schema( { device: { type: String, default: "" }, mountpoint: { type: String, default: "" }, - read_speed_bytes: { type: Number, default: 0 }, - write_speed_bytes: { type: Number, default: 0 }, total_bytes: { type: Number, default: 0 }, free_bytes: { type: Number, default: 0 }, + used_bytes: { type: Number, default: 0 }, usage_percent: { type: Number, default: 0 }, + total_inodes: { type: Number, default: 0 }, + free_inodes: { type: Number, default: 0 }, + used_inodes: { type: Number, default: 0 }, + inodes_usage_percent: { type: Number, default: 0 }, + read_bytes: { type: Number, default: 0 }, + write_bytes: { type: Number, default: 0 }, + read_time: { type: Number, default: 0 }, + write_time: { type: Number, default: 0 }, }, { _id: false } ); @@ -106,6 +114,7 @@ const hostSchema = new Schema( os: { type: String, default: "" }, platform: { type: String, default: "" }, kernel_version: { type: String, default: "" }, + pretty_name: { type: String, default: "" }, }, { _id: false } ); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 1f65673f3..64e8dbbb1 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -117,11 +117,18 @@ class MongoChecksRepository implements IChecksRepository { (disks ?? []).map((disk) => ({ device: disk?.device ?? "", mountpoint: disk?.mountpoint ?? "", - read_speed_bytes: disk?.read_speed_bytes ?? 0, - write_speed_bytes: disk?.write_speed_bytes ?? 0, total_bytes: disk?.total_bytes ?? 0, free_bytes: disk?.free_bytes ?? 0, + used_bytes: disk?.used_bytes ?? 0, usage_percent: disk?.usage_percent ?? 0, + total_inodes: disk?.total_inodes ?? 0, + free_inodes: disk?.free_inodes ?? 0, + used_inodes: disk?.used_inodes ?? 0, + inodes_usage_percent: disk?.inodes_usage_percent ?? 0, + read_bytes: disk?.read_bytes ?? 0, + write_bytes: disk?.write_bytes ?? 0, + read_time: disk?.read_time ?? 0, + write_time: disk?.write_time ?? 0, })); const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] => @@ -628,8 +635,8 @@ class MongoChecksRepository implements IChecksRepository { as: "dIdx", in: { name: { $concat: ["disk", { $toString: "$$dIdx" }] }, - readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, - writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, + readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_bytes", "$$dIdx"] } } } }, + writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_bytes", "$$dIdx"] } } } }, totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 8e223d7c8..757fb95e9 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -12,6 +12,7 @@ export interface CheckCpuInfo { physical_core?: number; logical_core?: number; frequency?: number; + current_frequency?: number; temperature?: number[]; free_percent?: number; usage_percent?: number; @@ -28,6 +29,7 @@ export interface CheckHostInfo { os?: string; platform?: string; kernel_version?: string; + pretty_name?: string; } export interface CheckCaptureInfo { @@ -38,11 +40,18 @@ export interface CheckCaptureInfo { export interface CheckDiskInfo { device?: string; mountpoint?: string; - read_speed_bytes?: number; - write_speed_bytes?: number; total_bytes?: number; free_bytes?: number; + used_bytes?: number; usage_percent?: number; + total_inodes?: number; + free_inodes?: number; + used_inodes?: number; + inodes_usage_percent?: number; + read_bytes?: number; + write_bytes?: number; + read_time?: number; + write_time?: number; } export interface CheckErrorInfo { diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 39fa9d9ac..57185a735 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -75,43 +75,51 @@ export interface UptimeDetailsResult { monitorStats: import("./monitorStats.js").MonitorStats | null; } +export interface HardwareDiskStats { + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; +} + +export interface HardwareNetStats { + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; +} + +export interface HardwareCheckStats { + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: HardwareDiskStats[]; + net: HardwareNetStats[]; +} + +export interface HardwareStats { + aggregateData: { + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: HardwareCheckStats[]; +} + export interface HardwareDetailsResult { monitor: Monitor; - stats: { - aggregateData: { - totalChecks: number; - }; - upChecks: { - totalChecks: number; - }; - checks: Array<{ - _id: string; - avgCpuUsage: number; - avgMemoryUsage: number; - avgTemperature: number[]; - disks: Array<{ - name: string; - readSpeed: number; - writeSpeed: number; - totalBytes: number; - freeBytes: number; - usagePercent: number; - }>; - net: Array<{ - name: string; - bytesSentPerSecond: number; - deltaBytesRecv: number; - deltaPacketsSent: number; - deltaPacketsRecv: number; - deltaErrIn: number; - deltaErrOut: number; - deltaDropIn: number; - deltaDropOut: number; - deltaFifoIn: number; - deltaFifoOut: number; - }>; - }>; - }; + stats: HardwareStats; monitorStats: import("./monitorStats.js").MonitorStats | null; }