gauges, charts

This commit is contained in:
Alex Holliday committed 2026-01-28 23:26:00 +00:00
1 parent dcf5a77923
commit cb392eaaba
14 files changed
+596 -86

No files matched your search

@@ -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";
}) => (
<defs>
<linearGradient
id={id}
x1={direction === "vertical" ? "0" : "0"}
y1={direction === "vertical" ? "0" : "0"}
x2={direction === "vertical" ? "0" : "1"}
y2={direction === "vertical" ? "1" : "0"}
>
<stop
offset="0%"
stopColor={startColor}
stopOpacity={startOpacity}
/>
<stop
offset="100%"
stopColor={endColor}
stopOpacity={endOpacity}
/>
</linearGradient>
</defs>
);
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 (
<BaseChart
icon={null}
title={title}
>
<ResponsiveContainer
width="100%"
height={200}
>
<AreaChart data={type === "temp" ? avgTemps : data}>
<XAxis
dataKey={xKey}
tick={(props) => (
<XTick
{...props}
range={range}
/>
)}
/>
<YAxis domain={type === "temp" ? tempYDomain : yDomain} />
<CartesianGrid
stroke={theme.palette.divider}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
{dataKeys?.map((dataKey, index) => {
const gradientId = `gradient-${uniqueId}-${index}`;
return (
<Fragment key={`${dataKey}-${index}`}>
{gradient === true &&
createGradient({
id: gradientId,
startColor: gradientStartColor || AREA_COLORS[index],
endColor: gradientEndColor || "transparent",
direction: gradientDirection,
})}
<Area
key={dataKey}
type="monotone"
dataKey={dataKey}
stroke={strokeColor || AREA_COLORS[index]}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
</Fragment>
);
})}
</AreaChart>
</ResponsiveContainer>
</BaseChart>
);
};
@@ -9,3 +9,4 @@ export * from "./charts/PiePageSpeed";
export * from "./charts/PiePageSpeedLegend";
export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
@@ -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 (
<Grid
container
spacing={theme.spacing(8)}
>
{chartConfigs.map((config) => {
return (
<Grid
size={isSmall ? 12 : 6}
key={`${config.type}-${config.idx ?? ""}`}
>
<HistogramInfrastructure
range={range}
title={config.title}
type={config.type}
idx={config.idx}
key={`${config.type}-${config.idx ?? ""}`}
checks={checks}
xKey="bucketDate"
yDomain={config.yDomain}
dataKeys={config.dataKeys}
gradient={true}
gradientStartColor={config.gradientStartColor}
gradientEndColor="#ffffff"
strokeColor={config.strokeColor}
/>
</Grid>
);
})}
</Grid>
);
};
@@ -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 (
<BaseChart
icon={null}
title={title}
maxWidth={225}
>
<Stack
alignItems={"center"}
mb={theme.spacing(4)}
gap={theme.spacing(4)}
>
<Gauge progress={progress} />
</Stack>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography>{upperLabel}</Typography>
<Typography>{upperValue}</Typography>
</Stack>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography>{lowerLabel}</Typography>
<Typography>{lowerValue}</Typography>
</Stack>
</BaseChart>
);
};
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 (
<Stack
direction={isSmall ? "column" : "row"}
spacing={theme.spacing(8)}
alignItems={"center"}
>
<InfraDetailGauge
title={t("pages.infrastructure.gauges.memory.title")}
progress={(snapshot?.memory?.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.memory.upperLabel")}
upperValue={getGbs(snapshot?.memory?.used_bytes || 0)}
lowerLabel={t("pages.infrastructure.gauges.memory.lowerLabel")}
lowerValue={getGbs(snapshot?.memory?.total_bytes || 0)}
/>
<InfraDetailGauge
title={t("pages.infrastructure.gauges.cpu.title")}
progress={(snapshot?.cpu?.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.cpu.upperLabel")}
upperValue={getFrequency(snapshot?.cpu?.current_frequency || 0)}
lowerLabel={t("pages.infrastructure.gauges.cpu.lowerLabel")}
lowerValue={getFrequency(snapshot?.cpu?.frequency || 0)}
/>
{snapshot?.disk?.map((disk, idx) => {
return (
<InfraDetailGauge
key={disk?.device || 0 + idx}
// title={`Disk ${idx} usage`}
title={t("pages.infrastructure.gauges.disk.title", { idx })}
progress={(disk.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.disk.upperLabel")}
upperValue={getGbs(disk?.used_bytes || 0)}
lowerLabel={t("pages.infrastructure.gauges.disk.lowerLabel")}
lowerValue={getGbs(disk?.total_bytes || 0)}
/>
);
})}
</Stack>
);
};
@@ -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 <StatusBoxes monitor={monitor} />;
const checks = stats?.checks || [];
return (
<Stack gap={theme.spacing(8)}>
<StatusBoxes monitor={monitor} />
<InfraDetailsGauges snapshot={monitor.recentChecks?.[0]} />
<InfraDetailsCharts
checks={checks}
range={"recent"}
/>
</Stack>
);
};
@@ -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 (
<BasePage>
@@ -68,7 +68,12 @@ const InfrastructureDetails = () => {
<Tab label={t("pages.infrastructure.tabs.labels.overview")} />
<Tab label={t("pages.infrastructure.tabs.labels.network")} />
</Tabs>
{selectedTab === 0 && <TabOverview monitor={monitor} />}
{selectedTab === 0 && (
<TabOverview
monitor={monitor}
stats={stats}
/>
)}
{selectedTab === 1 && <TabNetwork />}
</BasePage>
);
+10 -2
View File
@@ -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 {
+43 -35
View File
@@ -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;
}
-1
View File
@@ -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`;
+25
View File
@@ -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"
}
}
}
},
+11 -2
View File
@@ -71,6 +71,7 @@ const cpuSchema = new Schema<CheckCpuInfo>(
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<CheckDiskInfo>(
{
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<CheckHostInfo>(
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
pretty_name: { type: String, default: "" },
},
{ _id: false }
);
@@ -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"] } } } },
+11 -2
View File
@@ -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 {
+43 -35
View File
@@ -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;
}