Update VPN proxy client behavior
This commit is contained in:
@@ -75,7 +75,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const [alias, setAlias] = useState('');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [trafficScale, setTrafficScale] = useState<'linear' | 'log'>('linear');
|
||||
const [copyFeedback, setCopyFeedback] = useState<{ id: string; failed: boolean } | null>(null);
|
||||
const [copyFeedback, setCopyFeedback] = useState<Record<string, { failed: boolean }>>({});
|
||||
const [copyAnnouncement, setCopyAnnouncement] = useState<{ id: string; message: string } | null>(null);
|
||||
const [pencilAnimationId, setPencilAnimationId] = useState('');
|
||||
const [trafficDeltas, setTrafficDeltas] = useState<Record<string, TrafficDelta>>({});
|
||||
const deviceNodes = useRef(new Map<string, HTMLElement>());
|
||||
@@ -85,7 +86,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const movementAnimations = useRef(new Map<string, Animation>());
|
||||
const previousTraffic = useRef(new Map<string, { gateway: bigint; proxy: bigint }>());
|
||||
const aliasBaseline = useRef({ id: '', value: '' });
|
||||
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const copyTimers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
||||
const copyAttempts = useRef(new Map<string, object>());
|
||||
const trafficDeltaTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const trafficOrder = useRef<{ direction: 'asc' | 'desc'; ids: string[] }>({ direction: sortDirection, ids: [] });
|
||||
const devices = useMemo(
|
||||
@@ -104,7 +106,9 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (copyTimer.current) clearTimeout(copyTimer.current);
|
||||
for (const timer of copyTimers.current.values()) clearTimeout(timer);
|
||||
copyTimers.current.clear();
|
||||
copyAttempts.current.clear();
|
||||
if (trafficDeltaTimer.current) clearTimeout(trafficDeltaTimer.current);
|
||||
}, []);
|
||||
|
||||
@@ -190,14 +194,36 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
|
||||
async function copyDeviceIp(device: Device) {
|
||||
if (!device.ip) return;
|
||||
if (copyTimer.current) clearTimeout(copyTimer.current);
|
||||
const activeTimer = copyTimers.current.get(device.id);
|
||||
if (activeTimer) clearTimeout(activeTimer);
|
||||
const attempt = {};
|
||||
copyAttempts.current.set(device.id, attempt);
|
||||
let feedback: { failed: boolean };
|
||||
try {
|
||||
await copyText(device.ip);
|
||||
setCopyFeedback({ id: device.id, failed: false });
|
||||
feedback = { failed: false };
|
||||
} catch {
|
||||
setCopyFeedback({ id: device.id, failed: true });
|
||||
feedback = { failed: true };
|
||||
}
|
||||
copyTimer.current = setTimeout(() => setCopyFeedback(null), COPY_FEEDBACK_MS);
|
||||
if (copyAttempts.current.get(device.id) !== attempt) return;
|
||||
const announcement = {
|
||||
id: device.id,
|
||||
message: feedback.failed ? `Не удалось скопировать IP ${device.ip}` : `IP ${device.ip} скопирован`,
|
||||
};
|
||||
const pendingTimer = copyTimers.current.get(device.id);
|
||||
if (pendingTimer) clearTimeout(pendingTimer);
|
||||
setCopyFeedback((current) => ({ ...current, [device.id]: feedback }));
|
||||
setCopyAnnouncement(announcement);
|
||||
copyTimers.current.set(device.id, setTimeout(() => {
|
||||
setCopyFeedback((current) => {
|
||||
const next = { ...current };
|
||||
delete next[device.id];
|
||||
return next;
|
||||
});
|
||||
setCopyAnnouncement((current) => current?.id === device.id ? null : current);
|
||||
copyTimers.current.delete(device.id);
|
||||
copyAttempts.current.delete(device.id);
|
||||
}, COPY_FEEDBACK_MS));
|
||||
}
|
||||
|
||||
function startEditing(device: Device) {
|
||||
@@ -303,7 +329,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
)}
|
||||
|
||||
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать IP' : 'IP скопирован' : ''}
|
||||
{copyAnnouncement?.message || ''}
|
||||
</div>
|
||||
<div className="client-devices-list" aria-live="polite" aria-busy={status === 'loading' || status === 'refreshing'}>
|
||||
{devices.map((device) => {
|
||||
@@ -320,7 +346,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const totalTraffic = formatByteString((gatewayTotal + proxyTotal).toString());
|
||||
const hasProxyTraffic = proxyTotal > 0n;
|
||||
const trafficDelta = trafficDeltas[device.id] || {};
|
||||
const copied = copyFeedback?.id === device.id;
|
||||
const feedback = copyFeedback[device.id];
|
||||
const copied = Boolean(feedback);
|
||||
const policyBusy = device.policyStatus === 'applying';
|
||||
const policyFailed = device.policyStatus === 'failed';
|
||||
const policyPending = device.policyStatus === 'pending';
|
||||
@@ -392,7 +419,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
>{title}</button>}
|
||||
{(hasName || (editing && Boolean(alias))) && 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' : ''}`}
|
||||
className={`client-device-ip${copied ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать IP ${device.ip} устройства ${title}`}
|
||||
onClick={() => copyDeviceIp(device)}
|
||||
|
||||
Reference in New Issue
Block a user