Polish device traffic charts and bump Harbor client versions
This commit is contained in:
@@ -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">
|
||||
|
||||
+88
-5
@@ -922,9 +922,10 @@ p {
|
||||
}
|
||||
|
||||
.client-device {
|
||||
--client-device-chart-height: 34px;
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) 112px 34px;
|
||||
grid-template-rows: 34px 42px;
|
||||
grid-template-rows: 34px var(--client-device-chart-height);
|
||||
align-items: start;
|
||||
column-gap: 8px;
|
||||
row-gap: 6px;
|
||||
@@ -932,6 +933,10 @@ p {
|
||||
border-top: 1px solid color-mix(in oklch, var(--client-border) 72%, transparent);
|
||||
}
|
||||
|
||||
.client-device.is-pinned {
|
||||
--client-device-chart-height: 72px;
|
||||
}
|
||||
|
||||
.client-device-main {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
@@ -1256,11 +1261,13 @@ p {
|
||||
}
|
||||
|
||||
.client-device-traffic-breakdown > span {
|
||||
color: var(--client-accent);
|
||||
opacity: 0;
|
||||
filter: blur(5px);
|
||||
transform: translateY(-12px);
|
||||
transform-origin: top right;
|
||||
transition: opacity 180ms ease, filter 300ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
text-shadow: 0 0 3px var(--client-bg), 0 0 8px var(--client-bg);
|
||||
}
|
||||
|
||||
.client-device-traffic-breakdown > span:nth-child(2) {
|
||||
@@ -1332,11 +1339,16 @@ p {
|
||||
}
|
||||
|
||||
.client-device-traffic-breakdown .is-proxy {
|
||||
color: color-mix(in oklch, var(--client-accent) 76%, var(--client-text));
|
||||
}
|
||||
|
||||
.client-device-traffic-breakdown b {
|
||||
color: var(--client-accent);
|
||||
text-shadow: 0 0 3px var(--client-bg), 0 0 8px var(--client-bg);
|
||||
}
|
||||
|
||||
.client-device-traffic-breakdown .is-proxy b {
|
||||
color: color-mix(in oklch, var(--client-accent) 72%, var(--client-muted));
|
||||
color: color-mix(in oklch, var(--client-accent) 76%, var(--client-text));
|
||||
}
|
||||
|
||||
.client-device-traffic:focus-visible {
|
||||
@@ -1348,7 +1360,7 @@ p {
|
||||
.client-device-traffic-chart {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 2;
|
||||
height: 42px;
|
||||
height: var(--client-device-chart-height);
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
overflow: visible;
|
||||
@@ -1358,6 +1370,11 @@ p {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.client-device.is-pinned .client-device-traffic-chart {
|
||||
transform-origin: bottom center;
|
||||
animation: client-device-chart-expand 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.client-device-traffic-chart::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -1375,7 +1392,7 @@ p {
|
||||
grid-template-columns: repeat(var(--sample-count), minmax(1px, 1fr));
|
||||
align-items: flex-end;
|
||||
gap: 0;
|
||||
animation: client-device-traffic-shift 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
animation: client-device-traffic-shift 560ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-device-traffic-bar {
|
||||
@@ -1411,6 +1428,52 @@ p {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.client-device-traffic-bar.is-new .client-device-traffic-bar-fill {
|
||||
transform-origin: bottom center;
|
||||
animation: client-device-traffic-grow 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.client-device-traffic-axis {
|
||||
position: absolute;
|
||||
inset: 0 auto 14px 2px;
|
||||
z-index: 3;
|
||||
width: 1px;
|
||||
color: color-mix(in oklch, var(--client-text) 62%, var(--client-muted));
|
||||
font: 700 7.5px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 0 3px var(--client-bg), 0 0 7px var(--client-bg);
|
||||
}
|
||||
|
||||
.client-device-traffic-axis > span {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.client-device-traffic-axis > span::before {
|
||||
width: 5px;
|
||||
height: 1px;
|
||||
flex: 0 0 5px;
|
||||
background: currentColor;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.client-device-traffic-axis > .is-max {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.client-device-traffic-axis > .is-mid {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.client-device-traffic-axis > .is-zero {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.client-device-traffic-bar-fill > span {
|
||||
width: 100%;
|
||||
display: block;
|
||||
@@ -1489,10 +1552,28 @@ p {
|
||||
}
|
||||
|
||||
@keyframes client-device-traffic-shift {
|
||||
from { opacity: 0.52; transform: translateX(8px); }
|
||||
from { opacity: 0.72; transform: translateX(calc(100% / var(--sample-count))); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes client-device-traffic-grow {
|
||||
from { opacity: 0; transform: scaleY(0); }
|
||||
to { opacity: 1; transform: scaleY(1); }
|
||||
}
|
||||
|
||||
@keyframes client-device-chart-expand {
|
||||
from {
|
||||
opacity: 0.72;
|
||||
clip-path: inset(calc(100% - 34px) 0 0);
|
||||
transform: scaleY(0.48);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
clip-path: inset(0);
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.client-device-policy-wrap {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
@@ -4557,6 +4638,8 @@ p {
|
||||
.client-device-traffic-breakdown,
|
||||
.client-device-traffic-breakdown > span,
|
||||
.client-device-traffic-bar-tooltip,
|
||||
.client-device-traffic-bar-fill,
|
||||
.client-device-traffic-chart,
|
||||
.client-device-traffic-track,
|
||||
.client-device-edit,
|
||||
.client-device-edit svg,
|
||||
|
||||
@@ -51,6 +51,13 @@ export function trafficSampleMetrics(gatewayValue, proxyValue, maxValue, scale =
|
||||
};
|
||||
}
|
||||
|
||||
export function trafficAxisMid(maxValue, scale = 'linear') {
|
||||
const max = byteString(maxValue);
|
||||
if (!max) return 0n;
|
||||
if (scale !== 'log') return max / 2n;
|
||||
return BigInt(Math.max(1, Math.round(Math.expm1(Math.log1p(Number(max)) / 2))));
|
||||
}
|
||||
|
||||
export function sortDevicesByTraffic(devices, direction = 'desc') {
|
||||
const factor = direction === 'asc' ? 1 : -1;
|
||||
return (Array.isArray(devices) ? devices : [])
|
||||
|
||||
Reference in New Issue
Block a user