Polish device traffic charts and bump Harbor client versions
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-07 20:19:27 +03:00
parent ffd899033e
commit e6666558b7
5 changed files with 129 additions and 15 deletions
+20 -5
View File
@@ -7,6 +7,7 @@ import {
formatLastSeen,
positiveByteDelta,
sortDevicesByTraffic,
trafficAxisMid,
trafficSampleMetrics,
} from '../utils/format.js';
@@ -46,8 +47,9 @@ function TrafficChart({ samples, scale, capacity, routeLabel }) {
const total = byteString(sample.gatewayBytes) + byteString(sample.proxyBytes);
return total > largest ? total : largest;
}, 0n);
const mid = trafficAxisMid(max, scale);
const latest = samples.at(-1)?.observedAt || 'empty';
return <span className="client-device-traffic-chart" role="img" aria-label={`История трафика, шкала ${scale === 'log' ? 'логарифмическая' : 'линейная'}`}>
return <span className="client-device-traffic-chart" role="img" aria-label={`История трафика, шкала ${scale === 'log' ? 'логарифмическая' : 'линейная'}, максимум ${formatByteString(max)}`}>
<span className="client-device-traffic-track" key={latest} style={{ '--sample-count': Math.max(1, capacity) }}>
{samples.map((sample, index) => {
const gateway = byteString(sample.gatewayBytes);
@@ -57,8 +59,9 @@ function TrafficChart({ samples, scale, capacity, routeLabel }) {
const slot = capacity - samples.length + index;
const edge = slot < 28 ? ' is-edge-left' : slot >= capacity - 28 ? ' is-edge-right' : '';
const { height, gatewayShare } = trafficSampleMetrics(gateway, proxy, max, scale);
const newest = index === samples.length - 1;
return <i
className={`client-device-traffic-bar${edge}`}
className={`client-device-traffic-bar${edge}${newest ? ' is-new' : ''}`}
key={`${sample.observedAt}-${index}`}
style={{ height: `${height}%`, gridColumnStart: slot + 1 }}
>
@@ -75,6 +78,11 @@ function TrafficChart({ samples, scale, capacity, routeLabel }) {
</i>;
})}
</span>
{max > 0n && <span className="client-device-traffic-axis" aria-hidden="true">
<span className="is-max">{formatByteString(max)}</span>
<span className="is-mid">{formatByteString(mid)}</span>
<span className="is-zero">0</span>
</span>}
{samples.length > 0 && <span className="client-device-traffic-time" aria-hidden="true">
<time dateTime={samples[0].observedAt}>{chartTime(samples[0].observedAt)}</time>
<span>15 с</span>
@@ -98,6 +106,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
const [trafficDeltas, setTrafficDeltas] = useState({});
const deviceNodes = useRef(new Map());
const previousPositions = useRef(new Map());
const movementAnimations = useRef(new Map());
const previousTraffic = useRef(new Map());
const copyTimer = useRef(null);
const trafficDeltaTimer = useRef(null);
@@ -172,11 +181,13 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
useLayoutEffect(() => {
if (!open) {
previousPositions.current.clear();
for (const animation of movementAnimations.current.values()) animation.cancel();
movementAnimations.current.clear();
return;
}
const positions = new Map();
for (const [id, node] of deviceNodes.current) {
node.getAnimations().forEach((animation) => animation.cancel());
movementAnimations.current.get(id)?.cancel();
positions.set(id, node.getBoundingClientRect());
}
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
@@ -184,10 +195,14 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
const before = previousPositions.current.get(id);
const deltaY = before ? before.top - after.top : 0;
if (Math.abs(deltaY) < 1) continue;
deviceNodes.current.get(id)?.animate([
const animation = deviceNodes.current.get(id)?.animate([
{ transform: `translateY(${deltaY}px)` },
{ transform: 'translateY(0)' },
], { duration: DEVICE_MOVE_MS, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
if (animation) {
movementAnimations.current.set(id, animation);
animation.onfinish = () => movementAnimations.current.delete(id);
}
}
}
previousPositions.current = positions;
@@ -408,7 +423,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
if (node) deviceNodes.current.set(device.id, node);
else deviceNodes.current.delete(device.id);
}}
className={`client-device is-${device.status}`}
className={`client-device is-${device.status}${device.pinned ? ' is-pinned' : ''}`}
key={device.id}
>
<span className="client-device-pin-wrap client-tooltip-anchor">