mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-19 08:37:14 +00:00
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:
@@ -25,6 +25,8 @@ interface ApiMsg<T = unknown> {
|
|||||||
|
|
||||||
const REFRESH_MS = 15000;
|
const REFRESH_MS = 15000;
|
||||||
|
|
||||||
|
const formatKbps = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 1 });
|
||||||
|
|
||||||
export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanelProps) {
|
export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanelProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [cpuPoints, setCpuPoints] = useState<number[]>([]);
|
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
|
// 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') => {
|
const fetchSeries = async (metric: string, kind: 'pct' | 'rate') => {
|
||||||
try {
|
try {
|
||||||
const url = `/panel/api/nodes/history/${node.id}/${metric}/${bucket}`;
|
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}
|
fillOpacity={0.18}
|
||||||
markerRadius={2.6}
|
markerRadius={2.6}
|
||||||
showTooltip
|
showTooltip
|
||||||
|
valueMax={null}
|
||||||
|
yFormatter={formatKbps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="series">
|
<div className="series">
|
||||||
@@ -164,6 +168,8 @@ export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanel
|
|||||||
fillOpacity={0.18}
|
fillOpacity={0.18}
|
||||||
markerRadius={2.6}
|
markerRadius={2.6}
|
||||||
showTooltip
|
showTooltip
|
||||||
|
valueMax={null}
|
||||||
|
yFormatter={formatKbps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user