Track proxy traffic separately in device inventory
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import {
|
||||
byteString,
|
||||
formatByteString,
|
||||
formatLastSeen,
|
||||
sortDevicesByTraffic,
|
||||
@@ -210,6 +211,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
<h2 id="client-devices-title">Устройства</h2>
|
||||
<div className="client-instructions-intro">
|
||||
<p>Устройства, которые Gateway видит в локальной таблице соседей.</p>
|
||||
<p>Учитывается только трафик, который прошёл через Harbor.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -223,6 +225,11 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
Трафик временно не обновляется. Показаны последние сохранённые значения.
|
||||
</p>
|
||||
)}
|
||||
{snapshot?.source?.traffic?.proxy?.error && (
|
||||
<p className="client-devices-source" role="status">
|
||||
Прокси-трафик временно не обновляется. Показаны последние сохранённые значения.
|
||||
</p>
|
||||
)}
|
||||
{snapshot?.source?.policy?.error && (
|
||||
<p className="client-devices-source" role="status">
|
||||
Маршруты устройств временно не обновляются. Показано последнее подтверждённое состояние.
|
||||
@@ -248,6 +255,16 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const online = device.status === 'online';
|
||||
const download = formatByteString(device.downloadBytes);
|
||||
const upload = formatByteString(device.uploadBytes);
|
||||
const proxyDownload = formatByteString(device.proxyDownloadBytes);
|
||||
const proxyUpload = formatByteString(device.proxyUploadBytes);
|
||||
const gatewayTotal = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
||||
const proxyTotal = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||
const gatewayTraffic = formatByteString(gatewayTotal.toString());
|
||||
const proxyTraffic = formatByteString(proxyTotal.toString());
|
||||
const trafficLabel = [
|
||||
gatewayTotal > 0n ? `Gateway: получено ${download}, отдано ${upload}` : '',
|
||||
proxyTotal > 0n ? `Прокси: получено ${proxyDownload}, отдано ${proxyUpload}` : '',
|
||||
].filter(Boolean).join('. ');
|
||||
const policyBusy = device.policyStatus === 'applying';
|
||||
const policyFailed = device.policyStatus === 'failed';
|
||||
const policyPending = device.policyStatus === 'pending';
|
||||
@@ -314,11 +331,16 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
</div>
|
||||
)}
|
||||
<span className="client-device-traffic-slot">
|
||||
{device.trafficObservedAt && <span
|
||||
className="client-device-traffic"
|
||||
aria-label={`Получено ${download}, отдано ${upload}`}
|
||||
{(gatewayTotal > 0n || proxyTotal > 0n) && <span
|
||||
className="client-device-traffic client-tooltip-anchor"
|
||||
tabIndex="0"
|
||||
aria-label={trafficLabel}
|
||||
>
|
||||
<span aria-hidden="true">↓ {download} · ↑ {upload}</span>
|
||||
<span className="client-device-traffic-sources" aria-hidden="true">
|
||||
{gatewayTotal > 0n && <span>Gateway {gatewayTraffic}</span>}
|
||||
{proxyTotal > 0n && <span className="is-proxy">Прокси {proxyTraffic}</span>}
|
||||
</span>
|
||||
<Tooltip>{trafficLabel}</Tooltip>
|
||||
</span>}
|
||||
</span>
|
||||
<span className="client-device-pin-wrap client-tooltip-anchor">
|
||||
|
||||
@@ -1071,8 +1071,12 @@ p {
|
||||
}
|
||||
|
||||
.client-device-traffic {
|
||||
display: block;
|
||||
max-width: min(240px, 42vw);
|
||||
overflow: hidden;
|
||||
color: var(--client-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1081,6 +1085,44 @@ p {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.client-device-traffic-sources {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.client-device-traffic-sources span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.client-device-traffic-sources span + span::before {
|
||||
margin-right: 5px;
|
||||
color: var(--client-muted);
|
||||
content: '·';
|
||||
}
|
||||
|
||||
.client-device-traffic-sources .is-proxy {
|
||||
color: var(--client-accent);
|
||||
}
|
||||
|
||||
.client-device-traffic:focus-visible {
|
||||
border-radius: 3px;
|
||||
outline: 2px solid var(--client-accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.client-device-traffic.client-tooltip-anchor > .client-tooltip {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform: translate(0, 2px);
|
||||
}
|
||||
|
||||
.client-device-traffic.client-tooltip-anchor:hover > .client-tooltip,
|
||||
.client-device-traffic.client-tooltip-anchor:focus-visible > .client-tooltip {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.client-device-policy-wrap {
|
||||
width: 72px;
|
||||
position: relative;
|
||||
|
||||
@@ -36,8 +36,10 @@ export function sortDevicesByTraffic(devices, direction = 'desc') {
|
||||
return (Array.isArray(devices) ? devices : [])
|
||||
.map((device, index) => ({ device, index }))
|
||||
.sort((left, right) => {
|
||||
const leftTotal = byteString(left.device.uploadBytes) + byteString(left.device.downloadBytes);
|
||||
const rightTotal = byteString(right.device.uploadBytes) + byteString(right.device.downloadBytes);
|
||||
const leftTotal = byteString(left.device.uploadBytes) + byteString(left.device.downloadBytes)
|
||||
+ byteString(left.device.proxyUploadBytes) + byteString(left.device.proxyDownloadBytes);
|
||||
const rightTotal = byteString(right.device.uploadBytes) + byteString(right.device.downloadBytes)
|
||||
+ byteString(right.device.proxyUploadBytes) + byteString(right.device.proxyDownloadBytes);
|
||||
if (leftTotal === rightTotal) return left.index - right.index;
|
||||
return (leftTotal < rightTotal ? -1 : 1) * factor;
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user