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:
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"}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user