Enhance device traffic feedback and totals
This commit is contained in:
@@ -5,11 +5,14 @@ import {
|
||||
byteString,
|
||||
formatByteString,
|
||||
formatLastSeen,
|
||||
positiveByteDelta,
|
||||
sortDevicesByTraffic,
|
||||
} 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;
|
||||
|
||||
function Tooltip({ children }) {
|
||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
||||
@@ -24,6 +27,13 @@ function TextMorph({ from, to }) {
|
||||
</span>;
|
||||
}
|
||||
|
||||
function TrafficValue({ value, delta }) {
|
||||
return <strong className={`client-device-traffic-value${delta ? ' has-delta' : ''}`}>
|
||||
<span className="is-total">{value}</span>
|
||||
<span className="is-delta">{delta ? `+${delta}` : ''}</span>
|
||||
</strong>;
|
||||
}
|
||||
|
||||
export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const [snapshot, setSnapshot] = useState(null);
|
||||
const [status, setStatus] = useState('idle');
|
||||
@@ -35,9 +45,12 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const [refreshCycle, setRefreshCycle] = useState(0);
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||
const [trafficDeltas, setTrafficDeltas] = 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 devices = useMemo(
|
||||
() => sortDevicesByTraffic(snapshot?.devices, sortDirection),
|
||||
[snapshot?.devices, sortDirection],
|
||||
@@ -72,7 +85,37 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
return () => clearTimeout(timer);
|
||||
}, [open, refreshCycle, refreshing, status]);
|
||||
|
||||
useEffect(() => () => clearTimeout(copyTimer.current), []);
|
||||
useEffect(() => () => {
|
||||
clearTimeout(copyTimer.current);
|
||||
clearTimeout(trafficDeltaTimer.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
previousTraffic.current.clear();
|
||||
clearTimeout(trafficDeltaTimer.current);
|
||||
setTrafficDeltas({});
|
||||
return;
|
||||
}
|
||||
|
||||
const next = new Map();
|
||||
const deltas = {};
|
||||
for (const device of snapshot?.devices || []) {
|
||||
const gateway = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
||||
const proxy = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||
const previous = previousTraffic.current.get(device.id);
|
||||
next.set(device.id, { gateway, proxy });
|
||||
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 };
|
||||
}
|
||||
previousTraffic.current = next;
|
||||
if (!Object.keys(deltas).length) return;
|
||||
setTrafficDeltas(deltas);
|
||||
clearTimeout(trafficDeltaTimer.current);
|
||||
trafficDeltaTimer.current = setTimeout(() => setTrafficDeltas({}), TRAFFIC_DELTA_MS);
|
||||
}, [snapshot?.devices, open]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
@@ -171,7 +214,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
} catch {
|
||||
setCopyFeedback({ id: device.id, failed: true });
|
||||
}
|
||||
copyTimer.current = setTimeout(() => setCopyFeedback(null), 800);
|
||||
copyTimer.current = setTimeout(() => setCopyFeedback(null), COPY_FEEDBACK_MS);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -266,7 +309,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
)}
|
||||
|
||||
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
|
||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'copied!' : ''}
|
||||
</div>
|
||||
<div className="client-devices-list" aria-live="polite" aria-busy={status === 'loading' || status === 'refreshing'}>
|
||||
{devices.map((device) => {
|
||||
@@ -279,6 +322,8 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const proxyTotal = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||
const gatewayTraffic = formatByteString(gatewayTotal.toString());
|
||||
const proxyTraffic = formatByteString(proxyTotal.toString());
|
||||
const totalTraffic = formatByteString((gatewayTotal + proxyTotal).toString());
|
||||
const trafficDelta = trafficDeltas[device.id] || {};
|
||||
const copied = copyFeedback?.id === device.id;
|
||||
const policyBusy = device.policyStatus === 'applying';
|
||||
const policyFailed = device.policyStatus === 'failed';
|
||||
@@ -350,7 +395,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
<span className="client-device-name-primary">{title}</span>
|
||||
<span className="client-device-name-ip" aria-hidden="true">{device.ip}</span>
|
||||
<span className="client-device-name-feedback" aria-hidden="true">
|
||||
{copied && copyFeedback.failed ? 'Ошибка' : 'Скопировано'}
|
||||
{copied && copyFeedback.failed ? 'Ошибка' : 'copied!'}
|
||||
</span>
|
||||
</button>
|
||||
</h3> : <h3>{title}</h3>}
|
||||
@@ -384,8 +429,9 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
|
||||
<span className="client-device-traffic" role="group" aria-label={`Gateway ${gatewayTraffic}, Прокси ${proxyTraffic}`}>
|
||||
<span className="client-device-traffic-sources" aria-hidden="true">
|
||||
<span><b>Gateway</b><strong>{gatewayTraffic}</strong></span>
|
||||
<span className="is-proxy"><b>Прокси</b><strong>{proxyTraffic}</strong></span>
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user