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.
This commit is contained in:
Sanaei
2026-09-16 11:48:55 +02:00
parent 7ef22f94c9
commit b78dd82869
2 changed files with 61 additions and 1 deletions
@@ -25,6 +25,8 @@ interface ApiMsg<T = unknown> {
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<number[]>([]);
@@ -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}
/>
</div>
<div className="series">
@@ -164,6 +168,8 @@ export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanel
fillOpacity={0.18}
markerRadius={2.6}
showTooltip
valueMax={null}
yFormatter={formatKbps}
/>
</div>
</div>
@@ -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<string, number> = {
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(<NodeHistoryPanel node={{ id: 7 }} />);
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],
]);
});
});