Update VPN client connection flow
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
const AUTO_REFRESH_MS = 15_000;
|
||||
const DEVICE_MOVE_MS = 520;
|
||||
const COPY_FEEDBACK_MS = 5_000;
|
||||
const COPY_FEEDBACK_MS = 800;
|
||||
const TRAFFIC_DELTA_MS = 2_200;
|
||||
|
||||
function Tooltip({ children }) {
|
||||
@@ -45,13 +45,14 @@ function chartTime(value) {
|
||||
|
||||
function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
|
||||
const [hovered, setHovered] = useState(null);
|
||||
const previousPoints = useRef([]);
|
||||
const previousScale = useRef(scale);
|
||||
const max = samples.reduce((largest, sample) => {
|
||||
const gateway = byteString(sample.gatewayBytes);
|
||||
const proxy = byteString(sample.proxyBytes);
|
||||
return gateway > largest ? gateway : proxy > largest ? proxy : largest;
|
||||
}, 0n);
|
||||
const mid = trafficAxisMid(max, scale);
|
||||
const latest = samples.at(-1)?.observedAt || 'empty';
|
||||
const firstSlot = capacity - samples.length;
|
||||
const points = samples.map((sample, index) => {
|
||||
const gateway = byteString(sample.gatewayBytes);
|
||||
@@ -69,6 +70,15 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
|
||||
const penultimate = points.at(-2);
|
||||
const newest = points.at(-1);
|
||||
const hasProxy = points.some(({ proxy }) => proxy > 0n);
|
||||
const scaleFrom = previousPoints.current;
|
||||
const animateScale = previousScale.current !== scale
|
||||
&& scaleFrom.length === points.length
|
||||
&& !(typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
previousPoints.current = points;
|
||||
previousScale.current = scale;
|
||||
}, [points, scale]);
|
||||
|
||||
function trackPointer(event) {
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
@@ -111,12 +121,48 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
|
||||
<line x1="0" x2="100" y1="50" y2="50" />
|
||||
<line x1="0" x2="100" y1="100" y2="100" />
|
||||
</g>}
|
||||
<g key={latest} className="client-device-traffic-lines" style={{ '--sample-count': Math.max(1, capacity) }}>
|
||||
{previous.length > 0 && <polyline className="is-gateway" points={previous.map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')} />}
|
||||
{hasProxy && previous.length > 0 && <polyline className="is-proxy" points={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')} />}
|
||||
{penultimate && newest && <line className="is-gateway is-new" pathLength="1" x1={penultimate.x} y1={penultimate.gatewayY} x2={newest.x} y2={newest.gatewayY} />}
|
||||
{hasProxy && penultimate && newest && <line className="is-proxy is-new" pathLength="1" x1={penultimate.x} y1={penultimate.proxyY} x2={newest.x} y2={newest.proxyY} />}
|
||||
{!penultimate && newest && <circle className="is-gateway is-new" cx={newest.x} cy={newest.gatewayY} r="1.5" />}
|
||||
<g className="client-device-traffic-lines" style={{ '--sample-count': Math.max(1, capacity) }}>
|
||||
{previous.length > 0 && <polyline className="is-gateway" points={previous.map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}>
|
||||
{animateScale && <animate
|
||||
key={`gateway-${scale}`}
|
||||
attributeName="points"
|
||||
from={scaleFrom.slice(0, -1).map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}
|
||||
to={previous.map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}
|
||||
dur="520ms"
|
||||
calcMode="spline"
|
||||
keyTimes="0;1"
|
||||
keySplines="0.16 1 0.3 1"
|
||||
fill="freeze"
|
||||
/>}
|
||||
</polyline>}
|
||||
{hasProxy && previous.length > 0 && <polyline className="is-proxy" points={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}>
|
||||
{animateScale && <animate
|
||||
key={`proxy-${scale}`}
|
||||
attributeName="points"
|
||||
from={scaleFrom.slice(0, -1).map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}
|
||||
to={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}
|
||||
dur="520ms"
|
||||
calcMode="spline"
|
||||
keyTimes="0;1"
|
||||
keySplines="0.16 1 0.3 1"
|
||||
fill="freeze"
|
||||
/>}
|
||||
</polyline>}
|
||||
{penultimate && newest && <line className="is-gateway is-new" pathLength="1" x1={penultimate.x} y1={penultimate.gatewayY} x2={newest.x} y2={newest.gatewayY}>
|
||||
{animateScale && <>
|
||||
<animate key={`gateway-y1-${scale}`} attributeName="y1" from={scaleFrom.at(-2).gatewayY} to={penultimate.gatewayY} dur="520ms" fill="freeze" />
|
||||
<animate key={`gateway-y2-${scale}`} attributeName="y2" from={scaleFrom.at(-1).gatewayY} to={newest.gatewayY} dur="520ms" fill="freeze" />
|
||||
</>}
|
||||
</line>}
|
||||
{hasProxy && penultimate && newest && <line className="is-proxy is-new" pathLength="1" x1={penultimate.x} y1={penultimate.proxyY} x2={newest.x} y2={newest.proxyY}>
|
||||
{animateScale && <>
|
||||
<animate key={`proxy-y1-${scale}`} attributeName="y1" from={scaleFrom.at(-2).proxyY} to={penultimate.proxyY} dur="520ms" fill="freeze" />
|
||||
<animate key={`proxy-y2-${scale}`} attributeName="y2" from={scaleFrom.at(-1).proxyY} to={newest.proxyY} dur="520ms" fill="freeze" />
|
||||
</>}
|
||||
</line>}
|
||||
{!penultimate && newest && <circle className="is-gateway is-new" cx={newest.x} cy={newest.gatewayY} r="1.5">
|
||||
{animateScale && <animate key={`gateway-cy-${scale}`} attributeName="cy" from={scaleFrom[0].gatewayY} to={newest.gatewayY} dur="520ms" fill="freeze" />}
|
||||
</circle>}
|
||||
</g>
|
||||
{hovered && <g className="client-device-traffic-cursor">
|
||||
<line x1={hovered.x} x2={hovered.x} y1="0" y2="100" />
|
||||
@@ -335,6 +381,11 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
copyTimer.current = setTimeout(() => setCopyFeedback(null), COPY_FEEDBACK_MS);
|
||||
}
|
||||
|
||||
function startEditing(device) {
|
||||
setEditingId(device.id);
|
||||
setAlias(device.alias || device.hostname || '');
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
ref={panelRef}
|
||||
@@ -431,7 +482,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
)}
|
||||
|
||||
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'copied!' : ''}
|
||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать IP' : 'IP скопирован' : ''}
|
||||
</div>
|
||||
<div className="client-devices-list" aria-live="polite" aria-busy={status === 'loading' || status === 'refreshing'}>
|
||||
{devices.map((device) => {
|
||||
@@ -509,36 +560,34 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
{device.ip ? <h3 className="client-device-name-heading">
|
||||
<button
|
||||
className={`client-device-name${hasName ? '' : ' is-address-only'}${copied ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
<h3 className="client-device-name-heading">
|
||||
{hasName && <button
|
||||
className="client-device-alias-trigger"
|
||||
type="button"
|
||||
aria-label={`Изменить название ${title}`}
|
||||
onClick={() => startEditing(device)}
|
||||
>{title}</button>}
|
||||
{hasName && device.ip && <span className="client-device-name-separator" aria-hidden="true">·</span>}
|
||||
{device.ip ? <button
|
||||
className={`client-device-ip${copied ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать IP ${device.ip} устройства ${title}`}
|
||||
onClick={() => copyDeviceIp(device)}
|
||||
>
|
||||
<span className="client-device-name-primary">{title}</span>
|
||||
{hasName && <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!'}
|
||||
</span>
|
||||
</button>
|
||||
</h3> : <h3>{title}</h3>}
|
||||
<span className="client-device-edit-wrap client-tooltip-anchor">
|
||||
>{device.ip}</button> : !hasName && <span>Неизвестное устройство</span>}
|
||||
</h3>
|
||||
{!hasName && <span className="client-device-edit-wrap client-tooltip-anchor">
|
||||
<button
|
||||
className="client-device-edit"
|
||||
type="button"
|
||||
aria-label={`Изменить название ${title}`}
|
||||
onClick={() => {
|
||||
setEditingId(device.id);
|
||||
setAlias(device.alias || '');
|
||||
}}
|
||||
onClick={() => startEditing(device)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="m4 20 4.2-1 10.3-10.3a2 2 0 0 0-2.8-2.8L5.4 16.2 4 20ZM14.5 7.1l2.8 2.8" />
|
||||
</svg>
|
||||
</button>
|
||||
<Tooltip>Изменить название</Tooltip>
|
||||
</span>
|
||||
</span>}
|
||||
</>
|
||||
)}
|
||||
<span className={`client-device-last-seen${online ? ' is-online' : ''}`} tabIndex="0">
|
||||
|
||||
Reference in New Issue
Block a user