Refine device inventory animations and diagnostics layout
This commit is contained in:
@@ -192,9 +192,12 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const [trafficScale, setTrafficScale] = useState('linear');
|
||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||
const [pencilAnimationId, setPencilAnimationId] = useState('');
|
||||
const [trafficDeltas, setTrafficDeltas] = useState({});
|
||||
const deviceNodes = useRef(new Map());
|
||||
const previousPositions = useRef(new Map());
|
||||
const previousOrder = useRef([]);
|
||||
const previousScrollTop = useRef(0);
|
||||
const movementAnimations = useRef(new Map());
|
||||
const previousTraffic = useRef(new Map());
|
||||
const copyTimer = useRef(null);
|
||||
@@ -217,7 +220,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
const next = await (discover ? api.devices.refresh() : api.devices.list());
|
||||
setSnapshot((current) => !current || next.revision >= current.revision ? next : current);
|
||||
setSnapshot((current) => !current || next.revision > current.revision ? next : current);
|
||||
setError(null);
|
||||
setStatus('ready');
|
||||
} catch (requestError) {
|
||||
@@ -278,6 +281,8 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
previousPositions.current.clear();
|
||||
previousOrder.current = [];
|
||||
previousScrollTop.current = 0;
|
||||
for (const animation of movementAnimations.current.values()) animation.cancel();
|
||||
movementAnimations.current.clear();
|
||||
return;
|
||||
@@ -287,10 +292,17 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
movementAnimations.current.get(id)?.cancel();
|
||||
positions.set(id, node.getBoundingClientRect());
|
||||
}
|
||||
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
const order = devices.map(({ id }) => id);
|
||||
const orderChanged = previousOrder.current.length > 0
|
||||
&& (order.length !== previousOrder.current.length
|
||||
|| order.some((id, index) => id !== previousOrder.current[index]));
|
||||
const currentScrollTop = panelRef.current?.scrollTop || 0;
|
||||
if (orderChanged && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
for (const [id, after] of positions) {
|
||||
const before = previousPositions.current.get(id);
|
||||
const deltaY = before ? before.top - after.top : 0;
|
||||
const deltaY = before
|
||||
? before.top - after.top + previousScrollTop.current - currentScrollTop
|
||||
: 0;
|
||||
if (Math.abs(deltaY) < 1) continue;
|
||||
const animation = deviceNodes.current.get(id)?.animate([
|
||||
{ transform: `translateY(${deltaY}px)` },
|
||||
@@ -303,7 +315,9 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
}
|
||||
}
|
||||
previousPositions.current = positions;
|
||||
}, [devices, open]);
|
||||
previousOrder.current = order;
|
||||
previousScrollTop.current = currentScrollTop;
|
||||
}, [devices, open, panelRef]);
|
||||
|
||||
async function updateDevice(device, patch) {
|
||||
setSavingId(device.id);
|
||||
@@ -314,14 +328,14 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
} catch (requestError) {
|
||||
if (requestError.code !== 'STATE_CONFLICT') throw requestError;
|
||||
const latest = await api.devices.list();
|
||||
setSnapshot((current) => !current || latest.revision >= current.revision ? latest : current);
|
||||
setSnapshot((current) => !current || latest.revision > current.revision ? latest : current);
|
||||
const latestDevice = latest.devices.find((candidate) => candidate.id === device.id);
|
||||
if (!latestDevice || Object.keys(patch).some((key) => latestDevice[key] !== device[key])) {
|
||||
throw requestError;
|
||||
}
|
||||
next = await api.devices.update(device.id, patch, latest.revision);
|
||||
}
|
||||
setSnapshot((current) => !current || next.revision >= current.revision ? next : current);
|
||||
setSnapshot((current) => !current || next.revision > current.revision ? next : current);
|
||||
setError(null);
|
||||
return true;
|
||||
} catch (requestError) {
|
||||
@@ -347,18 +361,18 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
} catch (requestError) {
|
||||
if (requestError.code !== 'STATE_CONFLICT') throw requestError;
|
||||
const latest = await api.devices.list();
|
||||
setSnapshot((current) => !current || latest.revision >= current.revision ? latest : current);
|
||||
setSnapshot((current) => !current || latest.revision > current.revision ? latest : current);
|
||||
const latestDevice = latest.devices.find((candidate) => candidate.id === device.id);
|
||||
if (!latestDevice || latestDevice.desiredPolicy !== device.desiredPolicy) throw requestError;
|
||||
next = await api.devices.setPolicy(device.id, mode, latest.revision);
|
||||
}
|
||||
setSnapshot((current) => !current || next.revision >= current.revision ? next : current);
|
||||
setSnapshot((current) => !current || next.revision > current.revision ? next : current);
|
||||
setError(null);
|
||||
} catch (requestError) {
|
||||
if (requestError.code === 'DEVICE_POLICY_APPLY_FAILED') {
|
||||
try {
|
||||
const latest = await api.devices.list();
|
||||
setSnapshot((current) => !current || latest.revision >= current.revision ? latest : current);
|
||||
setSnapshot((current) => !current || latest.revision > current.revision ? latest : current);
|
||||
} catch {
|
||||
// Keep the policy error as the actionable result.
|
||||
}
|
||||
@@ -575,19 +589,25 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||
onClick={() => copyDeviceIp(device)}
|
||||
>{device.ip}</button> : !hasName && <span>Неизвестное устройство</span>}
|
||||
</h3>
|
||||
{!hasName && <span className="client-device-edit-wrap client-tooltip-anchor">
|
||||
<span className="client-device-edit-wrap client-tooltip-anchor">
|
||||
<button
|
||||
className="client-device-edit"
|
||||
className={`client-device-edit${pencilAnimationId === device.id ? ' is-writing' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Изменить название ${title}`}
|
||||
onPointerEnter={() => setPencilAnimationId(device.id)}
|
||||
onFocus={() => setPencilAnimationId(device.id)}
|
||||
onClick={() => startEditing(device)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
onAnimationEnd={() => setPencilAnimationId((id) => id === device.id ? '' : id)}
|
||||
>
|
||||
<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