Add device traffic history chart and total breakdown
This commit is contained in:
@@ -7,12 +7,14 @@ import {
|
||||
formatLastSeen,
|
||||
positiveByteDelta,
|
||||
sortDevicesByTraffic,
|
||||
trafficSampleMetrics,
|
||||
} from '../utils/format.js';
|
||||
|
||||
const AUTO_REFRESH_MS = 15_000;
|
||||
const DEVICE_MOVE_MS = 520;
|
||||
const COPY_FEEDBACK_MS = 5_000;
|
||||
const TRAFFIC_DELTA_MS = 2_200;
|
||||
const TRAFFIC_HISTORY_LIMIT = 18;
|
||||
|
||||
function Tooltip({ children }) {
|
||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
||||
@@ -34,6 +36,27 @@ function TrafficValue({ value, delta }) {
|
||||
</strong>;
|
||||
}
|
||||
|
||||
function TrafficChart({ samples }) {
|
||||
const max = samples.reduce((largest, sample) => {
|
||||
const total = byteString(sample.gateway) + byteString(sample.proxy);
|
||||
return total > largest ? total : largest;
|
||||
}, 0n);
|
||||
const latest = samples.at(-1)?.id || 'empty';
|
||||
return <span className="client-device-traffic-chart" aria-hidden="true">
|
||||
<span className="client-device-traffic-track" key={latest}>
|
||||
{samples.map((sample) => {
|
||||
const gateway = byteString(sample.gateway);
|
||||
const proxy = byteString(sample.proxy);
|
||||
const { height, gatewayShare } = trafficSampleMetrics(gateway, proxy, max);
|
||||
return <i className="client-device-traffic-bar" key={sample.id} style={{ height: `${height}%` }}>
|
||||
<span className="is-gateway" style={{ height: `${gatewayShare}%` }} />
|
||||
<span className="is-proxy" style={{ height: `${100 - gatewayShare}%` }} />
|
||||
</i>;
|
||||
})}
|
||||
</span>
|
||||
</span>;
|
||||
}
|
||||
|
||||
export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const [snapshot, setSnapshot] = useState(null);
|
||||
const [status, setStatus] = useState('idle');
|
||||
@@ -46,11 +69,13 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||
const [trafficDeltas, setTrafficDeltas] = useState({});
|
||||
const [trafficHistory, setTrafficHistory] = useState({});
|
||||
const deviceNodes = useRef(new Map());
|
||||
const previousPositions = useRef(new Map());
|
||||
const previousTraffic = useRef(new Map());
|
||||
const copyTimer = useRef(null);
|
||||
const trafficDeltaTimer = useRef(null);
|
||||
const trafficSequence = useRef(0);
|
||||
const devices = useMemo(
|
||||
() => sortDevicesByTraffic(snapshot?.devices, sortDirection),
|
||||
[snapshot?.devices, sortDirection],
|
||||
@@ -100,6 +125,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
|
||||
const next = new Map();
|
||||
const deltas = {};
|
||||
const historyChanges = {};
|
||||
for (const device of snapshot?.devices || []) {
|
||||
const gateway = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
||||
const proxy = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||
@@ -108,11 +134,25 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
if (!previous) continue;
|
||||
const gatewayDelta = positiveByteDelta(previous.gateway, gateway);
|
||||
const proxyDelta = positiveByteDelta(previous.proxy, proxy);
|
||||
if (gatewayDelta || proxyDelta) deltas[device.id] = { gateway: gatewayDelta, proxy: proxyDelta };
|
||||
if (!gatewayDelta && !proxyDelta) continue;
|
||||
const totalDelta = positiveByteDelta(previous.gateway + previous.proxy, gateway + proxy);
|
||||
deltas[device.id] = { gateway: gatewayDelta, proxy: proxyDelta, total: totalDelta };
|
||||
historyChanges[device.id] = {
|
||||
id: ++trafficSequence.current,
|
||||
gateway: gateway > previous.gateway ? (gateway - previous.gateway).toString() : '0',
|
||||
proxy: proxy > previous.proxy ? (proxy - previous.proxy).toString() : '0',
|
||||
};
|
||||
}
|
||||
previousTraffic.current = next;
|
||||
if (!Object.keys(deltas).length) return;
|
||||
setTrafficDeltas(deltas);
|
||||
setTrafficHistory((current) => {
|
||||
const updated = { ...current };
|
||||
for (const [id, change] of Object.entries(historyChanges)) {
|
||||
updated[id] = [...(current[id] || []), change].slice(-TRAFFIC_HISTORY_LIMIT);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
clearTimeout(trafficDeltaTimer.current);
|
||||
trafficDeltaTimer.current = setTimeout(() => setTrafficDeltas({}), TRAFFIC_DELTA_MS);
|
||||
}, [snapshot?.devices, open]);
|
||||
@@ -427,11 +467,13 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="client-device-traffic" role="group" aria-label={`Gateway ${gatewayTraffic}, Прокси ${proxyTraffic}`}>
|
||||
<span className="client-device-traffic-sources" aria-hidden="true">
|
||||
<span className="client-device-traffic" role="group" tabIndex="0" aria-label={`Всего ${totalTraffic}. Gateway ${gatewayTraffic}, Прокси ${proxyTraffic}`}>
|
||||
<span className="client-device-traffic-total" aria-hidden="true">
|
||||
<b>Всего</b><TrafficValue value={totalTraffic} delta={trafficDelta.total} />
|
||||
</span>
|
||||
<span className="client-device-traffic-breakdown" aria-hidden="true">
|
||||
<span><b>Gateway</b><TrafficValue value={gatewayTraffic} delta={trafficDelta.gateway} /></span>
|
||||
<span className="is-proxy"><b>Прокси</b><TrafficValue value={proxyTraffic} delta={trafficDelta.proxy} /></span>
|
||||
<span className="is-total"><b>Всего</b><strong>{totalTraffic}</strong></span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -454,6 +496,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
</button>
|
||||
<Tooltip>{policyTooltip}</Tooltip>
|
||||
</span>
|
||||
<TrafficChart samples={trafficHistory[device.id] || []} />
|
||||
</article>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user