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], + ]); + }); +});