Browse Source

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.
Sanaei 10 hours ago
parent
commit
b78dd82869

+ 7 - 1
frontend/src/pages/nodes/NodeHistoryPanel.tsx

@@ -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>

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