Add persistent device traffic history charts
This commit is contained in:
@@ -273,6 +273,51 @@ test('device traffic totals persist exact deltas across polls and process epochs
|
||||
assert.equal(snapshot.source.traffic.error, 'traffic unavailable');
|
||||
});
|
||||
|
||||
test('device traffic history stays in bounded service memory and survives client snapshots', async (t) => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-history-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const filePath = path.join(directory, 'devices.json');
|
||||
const store = createJsonStore({ filePath, defaultValue: {}, migrate: migrateDeviceInventoryState });
|
||||
const mac = '00:11:22:33:44:55';
|
||||
let observedAt = '2026-08-07T12:00:00.000Z';
|
||||
let uploadBytes = '100';
|
||||
let proxyDownloadBytes = '20';
|
||||
const createService = () => createDeviceInventoryService({
|
||||
store,
|
||||
observe: () => ({
|
||||
observedAt,
|
||||
error: null,
|
||||
observations: [{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true }],
|
||||
}),
|
||||
observeTraffic: () => ({
|
||||
epoch: 'epoch-a',
|
||||
generation: 'rules-a',
|
||||
observedAt,
|
||||
source: { error: null },
|
||||
devices: [{
|
||||
ip: '192.168.50.7', mac, interface: 'eth0', uploadBytes, downloadBytes: '0',
|
||||
proxyUploadBytes: '0', proxyDownloadBytes,
|
||||
}],
|
||||
}),
|
||||
});
|
||||
const service = createService();
|
||||
assert.deepEqual((await service.refresh()).devices[0].trafficHistory, []);
|
||||
|
||||
observedAt = '2026-08-07T12:00:15.000Z';
|
||||
uploadBytes = '130';
|
||||
proxyDownloadBytes = '25';
|
||||
const snapshot = await service.refresh();
|
||||
assert.equal(snapshot.trafficHistoryCapacity, 120);
|
||||
assert.deepEqual(snapshot.devices[0].trafficHistory, [{
|
||||
observedAt,
|
||||
gatewayBytes: '30',
|
||||
proxyBytes: '5',
|
||||
}]);
|
||||
assert.deepEqual(service.snapshot().devices[0].trafficHistory, snapshot.devices[0].trafficHistory);
|
||||
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /trafficHistory/);
|
||||
assert.deepEqual(createService().snapshot().devices[0].trafficHistory, []);
|
||||
});
|
||||
|
||||
test('legacy dataplane samples preserve saved proxy totals while Gateway totals keep advancing', async (t) => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-proxy-legacy-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
|
||||
@@ -52,8 +52,11 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.match(panel, /client-device-traffic-total[\s\S]*<b>Всего<\/b><TrafficValue value=\{totalTraffic\} delta=\{trafficDelta\.total\}/);
|
||||
assert.match(panel, /client-device-traffic-breakdown[\s\S]*<b>Gateway<\/b><TrafficValue value=\{gatewayTraffic\} delta=\{trafficDelta\.gateway\}/);
|
||||
assert.match(panel, /client-device-traffic-breakdown[\s\S]*className="is-proxy"><b>Прокси<\/b><TrafficValue value=\{proxyTraffic\} delta=\{trafficDelta\.proxy\}/);
|
||||
assert.match(panel, /TRAFFIC_HISTORY_LIMIT = 18[\s\S]*slice\(-TRAFFIC_HISTORY_LIMIT\)/);
|
||||
assert.match(panel, /TrafficChart samples=\{trafficHistory\[device\.id\] \|\| \[\]\}/);
|
||||
assert.match(panel, /TrafficChart[\s\S]*samples=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\.trafficHistoryCapacity/);
|
||||
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
|
||||
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
|
||||
assert.match(panel, /title=\{`\$\{time\} · Всего \$\{formatByteString\(total\)\} · Gateway/);
|
||||
assert.match(panel, /<time dateTime=\{samples\[0\]\.observedAt\}>[\s\S]*<span>15 с<\/span>/);
|
||||
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)/);
|
||||
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
|
||||
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
|
||||
@@ -71,7 +74,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
|
||||
assert.match(panel, /maxLength="64"[\s\S]*autoFocus/);
|
||||
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
|
||||
assert.match(styles, /\.client-device \{[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px 28px;[\s\S]*padding: 10px 8px/);
|
||||
assert.match(styles, /\.client-device \{[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px 42px;[\s\S]*padding: 10px 8px/);
|
||||
assert.match(styles, /\.client-device-main \{[\s\S]*display: flex;[\s\S]*align-items: center/);
|
||||
assert.match(styles, /\.client-device-traffic-total,[\s\S]*\.client-device-traffic-breakdown > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
|
||||
assert.match(styles, /\.client-device-traffic:hover \.client-device-traffic-breakdown,[\s\S]*opacity: 1[\s\S]*translateY\(0\)/);
|
||||
@@ -129,6 +132,7 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
|
||||
assert.deepEqual(trafficSampleMetrics('75', '25', '200'), { height: 50, gatewayShare: 75 });
|
||||
assert.deepEqual(trafficSampleMetrics('1', '0', '1000'), { height: 10, gatewayShare: 100 });
|
||||
assert.deepEqual(trafficSampleMetrics('0', '0', '0'), { height: 0, gatewayShare: 0 });
|
||||
assert.deepEqual(trafficSampleMetrics('10', '0', '100', 'log'), { height: 52, gatewayShare: 100 });
|
||||
|
||||
const devices = [
|
||||
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },
|
||||
|
||||
Reference in New Issue
Block a user