From b78dd8286910a8c049e661d345ec6cf5a7abfd52 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Wed, 16 Sep 2026 11:48:55 +0200 Subject: [PATCH] fix(nodes): chart node net throughput in KB/s, not percent The node history panel passed its Net Up / Net Down series to Sparkline without valueMax or yFormatter, so they inherited the percentage defaults: a fixed 0-100 scale and a "%" label. Any node above 100 KB/s drew off the top of the chart and every axis tick and tooltip read as a percentage. A Sparkline fed non-percentage data has to declare its own scale and unit; every other call site already did, only the two node net series did not. --- frontend/src/pages/nodes/NodeHistoryPanel.tsx | 8 ++- frontend/src/test/node-history-panel.test.tsx | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 frontend/src/test/node-history-panel.test.tsx diff --git a/frontend/src/pages/nodes/NodeHistoryPanel.tsx b/frontend/src/pages/nodes/NodeHistoryPanel.tsx index 2c46c19ee..1e339852d 100644 --- a/frontend/src/pages/nodes/NodeHistoryPanel.tsx +++ b/frontend/src/pages/nodes/NodeHistoryPanel.tsx @@ -25,6 +25,8 @@ interface ApiMsg { const REFRESH_MS = 15000; +const formatKbps = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 1 }); + export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanelProps) { const { t } = useTranslation(); const [cpuPoints, setCpuPoints] = useState([]); @@ -51,7 +53,7 @@ export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanel }; // cpu/mem are percentages (clamp 0-100); net throughput is bytes/sec shown - // as KB/s (no upper clamp, the sparkline auto-scales). + // as KB/s, which must opt out of Sparkline's 0-100 "%" defaults. const fetchSeries = async (metric: string, kind: 'pct' | 'rate') => { try { const url = `/panel/api/nodes/history/${node.id}/${metric}/${bucket}`; @@ -148,6 +150,8 @@ export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanel fillOpacity={0.18} markerRadius={2.6} showTooltip + valueMax={null} + yFormatter={formatKbps} />
@@ -164,6 +168,8 @@ export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanel fillOpacity={0.18} markerRadius={2.6} showTooltip + valueMax={null} + yFormatter={formatKbps} />
diff --git a/frontend/src/test/node-history-panel.test.tsx b/frontend/src/test/node-history-panel.test.tsx new file mode 100644 index 000000000..2e38c5173 --- /dev/null +++ b/frontend/src/test/node-history-panel.test.tsx @@ -0,0 +1,54 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import NodeHistoryPanel from '@/pages/nodes/NodeHistoryPanel'; +import { HttpUtil, Msg } from '@/utils'; + +const plots = vi.hoisted(() => [] as { scales: { y: { range: () => [number, number] } } }[]); + +vi.mock('uplot', () => ({ + default: class { + static paths = { spline: () => undefined }; + static pxRatio = 1; + constructor(opts: (typeof plots)[number]) { + plots.push(opts); + } + setData() {} + setSize() {} + redraw() {} + destroy() {} + }, +})); + +// The net series fell through to Sparkline's percentage defaults: a 0-100 scale +// and a "%" label, so 512 KB/s rendered as "512%" far above the chart. +describe('NodeHistoryPanel', () => { + it('charts net throughput in KB/s on its own scale', async () => { + const samples: Record = { + cpu: 40, + mem: 60, + netUp: 512 * 1024, + netDown: 200 * 1024, + }; + vi.spyOn(HttpUtil, 'get').mockImplementation(async (url: string) => { + const metric = url.split('/').at(-2) ?? ''; + return new Msg(true, '', [{ t: 1_700_000_000, v: samples[metric] }]); + }); + + render(); + + await waitFor(() => expect(screen.getAllByRole('img')).toHaveLength(4)); + expect(screen.getAllByRole('img').map((el) => el.getAttribute('aria-label'))).toEqual([ + '40%', + '60%', + '512', + '200', + ]); + expect(plots.map((p) => p.scales.y.range())).toEqual([ + [0, 100], + [0, 100], + [0, 512 * 1.1], + [0, 200 * 1.1], + ]); + }); +});