feat(client): diagnostics page polish

- Stats and StatGauges use CSS grid (5 + 4 columns) so cards in each
  row are equal width; widths overridden against the legacy 25%-flex
  GradientBox so they fill their grid cell.
- DetailGauge gains a maxWidth prop (default 225) so consumers in
  wide grids can opt out of the cap.
- Section explainers above each row: "Runtime stats" + description,
  "Memory & CPU" + description (i18n driven, propagated to all 16
  locales via pages.logs.diagnostics.sections).
- Both grids render immediately with placeholder "—" values, so the
  initial paint shows the layout and values fill in as data loads
  (eliminates the layout shake when switching to the tab).
This commit is contained in:
gorkem-bwl committed 2026-04-25 23:27:35 -04:00
1 parent 262c4eee93
commit 752f59b09b
4 files changed
+113 -48

No files matched your search

@@ -114,6 +114,7 @@ export const DetailGauge = ({
upperValue,
lowerLabel,
lowerValue,
maxWidth = 225,
}: {
title: string;
progress: number;
@@ -121,13 +122,14 @@ export const DetailGauge = ({
upperValue?: string | number;
lowerLabel?: string;
lowerValue?: string | number;
maxWidth?: number;
}) => {
const theme = useTheme();
return (
<BaseChart
icon={null}
title={title}
maxWidth={225}
maxWidth={maxWidth}
>
<Stack
alignItems={"center"}
+32 -3
View File
@@ -1,13 +1,16 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Stats } from "@/Pages/Logs/components/Stats";
import { StatGauges } from "@/Pages/Logs/components/StatGauges";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useGet } from "@/Hooks/UseApi";
import type { Diagnostics } from "@/Types/Diagnostics";
export const TabDiagnostics = () => {
const theme = useTheme();
const { t } = useTranslation();
const {
data: diagnostics,
isLoading: _isLoading,
@@ -16,9 +19,35 @@ export const TabDiagnostics = () => {
} = useGet<Diagnostics>("/diagnostic/system", {}, { refreshInterval: 5000 });
return (
<Stack gap={theme.spacing(8)}>
<Stats diagnostics={diagnostics} />
<StatGauges diagnostics={diagnostics} />
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(4)}>
<Stack gap={theme.spacing(1)}>
<Typography
variant="h6"
sx={{ textTransform: "uppercase" }}
>
{t("pages.logs.diagnostics.sections.runtimeStats.title")}
</Typography>
<Typography color={theme.palette.text.secondary}>
{t("pages.logs.diagnostics.sections.runtimeStats.description")}
</Typography>
</Stack>
<Stats diagnostics={diagnostics} />
</Stack>
<Stack gap={theme.spacing(4)}>
<Stack gap={theme.spacing(1)}>
<Typography
variant="h6"
sx={{ textTransform: "uppercase" }}
>
{t("pages.logs.diagnostics.sections.memoryCpu.title")}
</Typography>
<Typography color={theme.palette.text.secondary}>
{t("pages.logs.diagnostics.sections.memoryCpu.description")}
</Typography>
</Stack>
<StatGauges diagnostics={diagnostics} />
</Stack>
</Stack>
);
};
+45 -29
View File
@@ -1,4 +1,4 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { DetailGauge } from "@/Components/design-elements";
import { getPercentage, formatPercentageFromWhole } from "@/Utils/FormatUtils";
@@ -11,62 +11,78 @@ interface StatGaugesProps {
diagnostics: Diagnostics | null;
}
const PLACEHOLDER = "—";
export const StatGauges = ({ diagnostics }: StatGaugesProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (!diagnostics) {
return null;
}
const heapTotalSize = getPercentage(
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
diagnostics?.v8HeapStats?.heapSizeLimitBytes
);
const heapTotalSize = diagnostics
? getPercentage(
diagnostics.v8HeapStats?.totalHeapSizeBytes,
diagnostics.v8HeapStats?.heapSizeLimitBytes
)
: 0;
const heapUsedSize = diagnostics
? getPercentage(
diagnostics.v8HeapStats?.usedHeapSizeBytes,
diagnostics.v8HeapStats?.heapSizeLimitBytes
)
: 0;
const actualHeapUsed = diagnostics
? getPercentage(
diagnostics.v8HeapStats?.usedHeapSizeBytes,
diagnostics.v8HeapStats?.totalHeapSizeBytes
)
: 0;
const cpuUsage = diagnostics?.cpuUsage?.usagePercentage ?? 0;
const heapUsedSize = getPercentage(
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
diagnostics?.v8HeapStats?.heapSizeLimitBytes
);
const actualHeapUsed = getPercentage(
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
diagnostics?.v8HeapStats?.totalHeapSizeBytes
);
const fmt = (n: number) => (diagnostics ? formatPercentageFromWhole(n) : PLACEHOLDER);
const bytes = (n: number | undefined) =>
diagnostics ? prettyBytes(n ?? 0) : PLACEHOLDER;
return (
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(8)}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(4, 1fr)" },
gap: theme.spacing(8),
"& > *": { width: "100% !important" },
}}
>
<DetailGauge
maxWidth={9999}
title={t("pages.logs.diagnostics.gauges.heapAllocation")}
progress={heapTotalSize}
upperValue={formatPercentageFromWhole(heapTotalSize)}
upperValue={fmt(heapTotalSize)}
lowerLabel={t("pages.logs.diagnostics.gauges.total")}
lowerValue={prettyBytes(diagnostics.v8HeapStats?.heapSizeLimitBytes ?? 0)}
lowerValue={bytes(diagnostics?.v8HeapStats?.heapSizeLimitBytes)}
/>
<DetailGauge
maxWidth={9999}
title={t("pages.logs.diagnostics.gauges.heapUsage")}
progress={heapUsedSize}
upperLabel={t("pages.logs.diagnostics.gauges.availableMemoryPercentage")}
upperValue={formatPercentageFromWhole(heapUsedSize)}
upperValue={fmt(heapUsedSize)}
lowerLabel={t("pages.logs.diagnostics.gauges.used")}
lowerValue={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
lowerValue={bytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
/>
<DetailGauge
maxWidth={9999}
title={t("pages.logs.diagnostics.gauges.heapUtilization")}
progress={actualHeapUsed}
upperLabel={t("pages.logs.diagnostics.gauges.allocatedPercentage")}
upperValue={formatPercentageFromWhole(actualHeapUsed)}
upperValue={fmt(actualHeapUsed)}
lowerLabel={t("pages.logs.diagnostics.gauges.total")}
lowerValue={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
lowerValue={bytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
/>
<DetailGauge
maxWidth={9999}
title={t("pages.logs.diagnostics.gauges.instantCpuUsage")}
progress={diagnostics.cpuUsage?.usagePercentage ?? 0}
progress={cpuUsage}
upperLabel={t("pages.logs.diagnostics.gauges.usedSPercentage")}
upperValue={formatPercentageFromWhole(diagnostics.cpuUsage?.usagePercentage ?? 0)}
upperValue={fmt(cpuUsage)}
/>
</Stack>
</Box>
);
};
+33 -15
View File
@@ -1,4 +1,4 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { StatBox } from "@/Components/design-elements";
import prettyBytes from "pretty-bytes";
@@ -11,41 +11,59 @@ interface StatsProps {
diagnostics: Diagnostics | null;
}
const PLACEHOLDER = "—";
export const Stats = ({ diagnostics }: StatsProps) => {
const { t } = useTranslation();
const theme = useTheme();
if (!diagnostics) {
return null;
}
const eventLoopDelay = diagnostics
? prettyMilliseconds(diagnostics.eventLoopDelayMs ?? 0, {
millisecondsDecimalDigits: 2,
})
: PLACEHOLDER;
const uptime = diagnostics
? prettyMilliseconds(diagnostics.uptimeMs ?? 0, { hideSeconds: true })
: PLACEHOLDER;
const usedHeap = diagnostics
? prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)
: PLACEHOLDER;
const totalHeap = diagnostics
? prettyBytes(diagnostics.v8HeapStats?.totalHeapSizeBytes ?? 0)
: PLACEHOLDER;
const osMemory = diagnostics
? prettyBytes(diagnostics.osStats?.totalMemoryBytes ?? 0)
: PLACEHOLDER;
return (
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(8)}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(5, 1fr)" },
gap: theme.spacing(8),
"& > *": { width: "100% !important" },
}}
>
<StatBox
title={t("pages.logs.diagnostics.stats.eventLoopDelay")}
subtitle={prettyMilliseconds(diagnostics.eventLoopDelayMs ?? 0, {
millisecondsDecimalDigits: 2,
})}
subtitle={eventLoopDelay}
/>
<StatBox
title={t("pages.logs.diagnostics.stats.uptime")}
subtitle={prettyMilliseconds(diagnostics.uptimeMs ?? 0, { hideSeconds: true })}
subtitle={uptime}
/>
<StatBox
title={t("pages.logs.diagnostics.stats.usedHeapSize")}
subtitle={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
subtitle={usedHeap}
/>
<StatBox
title={t("pages.logs.diagnostics.stats.totalHeapSize")}
subtitle={prettyBytes(diagnostics.v8HeapStats?.totalHeapSizeBytes ?? 0)}
subtitle={totalHeap}
/>
<StatBox
title={t("pages.logs.diagnostics.stats.osMemoryLimit")}
subtitle={prettyBytes(diagnostics.osStats?.totalMemoryBytes ?? 0)}
subtitle={osMemory}
/>
</Stack>
</Box>
);
};