Refine device inventory animations and diagnostics layout
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.16.1',
|
macClient: '0.16.2',
|
||||||
gatewayClient: '0.17.1',
|
gatewayClient: '0.17.2',
|
||||||
gatewayBackend: '0.17.1',
|
gatewayBackend: '0.17.1',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,19 +63,6 @@ function IpCell({ path, source, pending, route }) {
|
|||||||
return <code aria-label={`${route}: ${value.address}`}>{value.address}</code>;
|
return <code aria-label={`${route}: ${value.address}`}>{value.address}</code>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PathDetails({ title, path }) {
|
|
||||||
if (!path?.available) return <section><strong>{title}</strong><p>VPN не запущен.</p></section>;
|
|
||||||
return <section>
|
|
||||||
<strong>{title}</strong>
|
|
||||||
{path.sites.map((site) => (
|
|
||||||
<p key={site.id}>
|
|
||||||
{site.label}: {site.stage}{site.httpStatus ? ` · HTTP ${site.httpStatus}` : ''}
|
|
||||||
{site.error ? ` · ${site.error}` : ''}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</section>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose }) {
|
export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose }) {
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
@@ -172,7 +159,7 @@ export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="client-diagnostics-feedback" aria-live="polite">
|
{(error || result) && <div className="client-diagnostics-feedback" aria-live="polite">
|
||||||
{error ? <div className="client-diagnostics-error" role="alert">
|
{error ? <div className="client-diagnostics-error" role="alert">
|
||||||
<span>{error.message}</span>
|
<span>{error.message}</span>
|
||||||
{error.retryable && <button type="button" onClick={run}>Повторить</button>}
|
{error.retryable && <button type="button" onClick={run}>Повторить</button>}
|
||||||
@@ -180,7 +167,7 @@ export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose
|
|||||||
<strong className={`client-diagnostics-summary ${summary[0]}`}>{summary[1]}</strong>
|
<strong className={`client-diagnostics-summary ${summary[0]}`}>{summary[1]}</strong>
|
||||||
<span className="client-diagnostics-time">{checkedAt}</span>
|
<span className="client-diagnostics-time">{checkedAt}</span>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>}
|
||||||
|
|
||||||
<section className="client-diagnostics-section" aria-labelledby="diagnostic-ip-title">
|
<section className="client-diagnostics-section" aria-labelledby="diagnostic-ip-title">
|
||||||
<div className="client-diagnostics-section-title">
|
<div className="client-diagnostics-section-title">
|
||||||
@@ -266,13 +253,6 @@ export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{result && <details className="client-diagnostics-details">
|
|
||||||
<summary>Технические детали</summary>
|
|
||||||
<div>
|
|
||||||
<PathDetails title="Напрямую" path={result.direct} />
|
|
||||||
<PathDetails title="VPN" path={result.vpn} />
|
|
||||||
</div>
|
|
||||||
</details>}
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -192,9 +192,12 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
const [sortDirection, setSortDirection] = useState('desc');
|
const [sortDirection, setSortDirection] = useState('desc');
|
||||||
const [trafficScale, setTrafficScale] = useState('linear');
|
const [trafficScale, setTrafficScale] = useState('linear');
|
||||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||||
|
const [pencilAnimationId, setPencilAnimationId] = useState('');
|
||||||
const [trafficDeltas, setTrafficDeltas] = useState({});
|
const [trafficDeltas, setTrafficDeltas] = useState({});
|
||||||
const deviceNodes = useRef(new Map());
|
const deviceNodes = useRef(new Map());
|
||||||
const previousPositions = useRef(new Map());
|
const previousPositions = useRef(new Map());
|
||||||
|
const previousOrder = useRef([]);
|
||||||
|
const previousScrollTop = useRef(0);
|
||||||
const movementAnimations = useRef(new Map());
|
const movementAnimations = useRef(new Map());
|
||||||
const previousTraffic = useRef(new Map());
|
const previousTraffic = useRef(new Map());
|
||||||
const copyTimer = useRef(null);
|
const copyTimer = useRef(null);
|
||||||
@@ -217,7 +220,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
const next = await (discover ? api.devices.refresh() : api.devices.list());
|
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);
|
setError(null);
|
||||||
setStatus('ready');
|
setStatus('ready');
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
@@ -278,6 +281,8 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
previousPositions.current.clear();
|
previousPositions.current.clear();
|
||||||
|
previousOrder.current = [];
|
||||||
|
previousScrollTop.current = 0;
|
||||||
for (const animation of movementAnimations.current.values()) animation.cancel();
|
for (const animation of movementAnimations.current.values()) animation.cancel();
|
||||||
movementAnimations.current.clear();
|
movementAnimations.current.clear();
|
||||||
return;
|
return;
|
||||||
@@ -287,10 +292,17 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
movementAnimations.current.get(id)?.cancel();
|
movementAnimations.current.get(id)?.cancel();
|
||||||
positions.set(id, node.getBoundingClientRect());
|
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) {
|
for (const [id, after] of positions) {
|
||||||
const before = previousPositions.current.get(id);
|
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;
|
if (Math.abs(deltaY) < 1) continue;
|
||||||
const animation = deviceNodes.current.get(id)?.animate([
|
const animation = deviceNodes.current.get(id)?.animate([
|
||||||
{ transform: `translateY(${deltaY}px)` },
|
{ transform: `translateY(${deltaY}px)` },
|
||||||
@@ -303,7 +315,9 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousPositions.current = positions;
|
previousPositions.current = positions;
|
||||||
}, [devices, open]);
|
previousOrder.current = order;
|
||||||
|
previousScrollTop.current = currentScrollTop;
|
||||||
|
}, [devices, open, panelRef]);
|
||||||
|
|
||||||
async function updateDevice(device, patch) {
|
async function updateDevice(device, patch) {
|
||||||
setSavingId(device.id);
|
setSavingId(device.id);
|
||||||
@@ -314,14 +328,14 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
if (requestError.code !== 'STATE_CONFLICT') throw requestError;
|
if (requestError.code !== 'STATE_CONFLICT') throw requestError;
|
||||||
const latest = await api.devices.list();
|
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);
|
const latestDevice = latest.devices.find((candidate) => candidate.id === device.id);
|
||||||
if (!latestDevice || Object.keys(patch).some((key) => latestDevice[key] !== device[key])) {
|
if (!latestDevice || Object.keys(patch).some((key) => latestDevice[key] !== device[key])) {
|
||||||
throw requestError;
|
throw requestError;
|
||||||
}
|
}
|
||||||
next = await api.devices.update(device.id, patch, latest.revision);
|
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);
|
setError(null);
|
||||||
return true;
|
return true;
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
@@ -347,18 +361,18 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
if (requestError.code !== 'STATE_CONFLICT') throw requestError;
|
if (requestError.code !== 'STATE_CONFLICT') throw requestError;
|
||||||
const latest = await api.devices.list();
|
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);
|
const latestDevice = latest.devices.find((candidate) => candidate.id === device.id);
|
||||||
if (!latestDevice || latestDevice.desiredPolicy !== device.desiredPolicy) throw requestError;
|
if (!latestDevice || latestDevice.desiredPolicy !== device.desiredPolicy) throw requestError;
|
||||||
next = await api.devices.setPolicy(device.id, mode, latest.revision);
|
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);
|
setError(null);
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
if (requestError.code === 'DEVICE_POLICY_APPLY_FAILED') {
|
if (requestError.code === 'DEVICE_POLICY_APPLY_FAILED') {
|
||||||
try {
|
try {
|
||||||
const latest = await api.devices.list();
|
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 {
|
} catch {
|
||||||
// Keep the policy error as the actionable result.
|
// Keep the policy error as the actionable result.
|
||||||
}
|
}
|
||||||
@@ -575,19 +589,25 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
onClick={() => copyDeviceIp(device)}
|
onClick={() => copyDeviceIp(device)}
|
||||||
>{device.ip}</button> : !hasName && <span>Неизвестное устройство</span>}
|
>{device.ip}</button> : !hasName && <span>Неизвестное устройство</span>}
|
||||||
</h3>
|
</h3>
|
||||||
{!hasName && <span className="client-device-edit-wrap client-tooltip-anchor">
|
<span className="client-device-edit-wrap client-tooltip-anchor">
|
||||||
<button
|
<button
|
||||||
className="client-device-edit"
|
className={`client-device-edit${pencilAnimationId === device.id ? ' is-writing' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Изменить название ${title}`}
|
aria-label={`Изменить название ${title}`}
|
||||||
|
onPointerEnter={() => setPencilAnimationId(device.id)}
|
||||||
|
onFocus={() => setPencilAnimationId(device.id)}
|
||||||
onClick={() => startEditing(device)}
|
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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<Tooltip>Изменить название</Tooltip>
|
<Tooltip>Изменить название</Tooltip>
|
||||||
</span>}
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span className={`client-device-last-seen${online ? ' is-online' : ''}`} tabIndex="0">
|
<span className={`client-device-last-seen${online ? ' is-online' : ''}`} tabIndex="0">
|
||||||
|
|||||||
+15
-47
@@ -946,7 +946,7 @@ p {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding-top: 11px;
|
padding: 11px 27px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-main > h3 {
|
.client-device-main > h3 {
|
||||||
@@ -1040,10 +1040,11 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-edit-wrap {
|
.client-device-edit-wrap {
|
||||||
flex: 0 0 auto;
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
width: 23px;
|
width: 23px;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
align-self: flex-end;
|
|
||||||
opacity: 0.34;
|
opacity: 0.34;
|
||||||
transition: opacity 180ms ease, filter 240ms ease, transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: opacity 180ms ease, filter 240ms ease, transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
@@ -1093,9 +1094,16 @@ p {
|
|||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-edit:hover svg,
|
.client-device-edit.is-writing svg {
|
||||||
.client-device-edit:focus-visible svg {
|
animation: client-device-pencil-write 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
transform: translate(1px, -1px) rotate(-4deg);
|
}
|
||||||
|
|
||||||
|
@keyframes client-device-pencil-write {
|
||||||
|
0%, 100% { transform: translate(0, 0) rotate(0); }
|
||||||
|
22% { transform: translate(1px, -1px) rotate(-5deg); }
|
||||||
|
44% { transform: translate(-1px, 1px) rotate(-2deg); }
|
||||||
|
66% { transform: translate(1px, 0) rotate(-5deg); }
|
||||||
|
84% { transform: translate(0, 1px) rotate(-2deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-pin[aria-pressed="true"] {
|
.client-device-pin[aria-pressed="true"] {
|
||||||
@@ -4624,7 +4632,6 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-feedback {
|
.client-diagnostics-feedback {
|
||||||
min-height: 34px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -4838,43 +4845,6 @@ p {
|
|||||||
color: oklch(0.68 0.15 28);
|
color: oklch(0.68 0.15 28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-details {
|
|
||||||
margin: 0 8px;
|
|
||||||
color: var(--client-muted);
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-diagnostics-details summary {
|
|
||||||
width: fit-content;
|
|
||||||
padding: 6px 0;
|
|
||||||
color: var(--client-text);
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-diagnostics-details summary:focus-visible {
|
|
||||||
outline: 2px solid var(--client-accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-diagnostics-details > div {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 18px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-diagnostics-details section {
|
|
||||||
min-width: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-diagnostics-details p {
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.client-diagnostics-table th,
|
.client-diagnostics-table th,
|
||||||
.client-diagnostics-table td {
|
.client-diagnostics-table td {
|
||||||
@@ -4889,10 +4859,8 @@ p {
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-diagnostics-add-action,
|
.client-diagnostics-add-action {
|
||||||
.client-diagnostics-details > div {
|
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*api\.devices\.list\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*api\.devices\.update\(device\.id, patch, latest\.revision\)/);
|
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*api\.devices\.list\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*api\.devices\.update\(device\.id, patch, latest\.revision\)/);
|
||||||
assert.match(panel, /deviceNodes\.current\.get\(id\)\?\.animate/);
|
assert.match(panel, /deviceNodes\.current\.get\(id\)\?\.animate/);
|
||||||
assert.match(panel, /movementAnimations\.current\.get\(id\)\?\.cancel\(\)/);
|
assert.match(panel, /movementAnimations\.current\.get\(id\)\?\.cancel\(\)/);
|
||||||
|
assert.match(panel, /const orderChanged = previousOrder\.current\.length > 0/);
|
||||||
|
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
|
||||||
|
assert.match(panel, /next\.revision > current\.revision/);
|
||||||
|
assert.doesNotMatch(panel, /revision >= current\.revision/);
|
||||||
assert.match(panel, /prefers-reduced-motion: reduce/);
|
assert.match(panel, /prefers-reduced-motion: reduce/);
|
||||||
assert.match(api, /refresh: \(\) => request\('\/api\/devices\/refresh', \{ method: 'POST' \}\)/);
|
assert.match(api, /refresh: \(\) => request\('\/api\/devices\/refresh', \{ method: 'POST' \}\)/);
|
||||||
assert.match(api, /setPolicy: \(id, mode, expectedRevision\) => request\(`\/api\/devices\/\$\{id\}\/policy`/);
|
assert.match(api, /setPolicy: \(id, mode, expectedRevision\) => request\(`\/api\/devices\/\$\{id\}\/policy`/);
|
||||||
@@ -38,7 +42,10 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /const hasName = Boolean\(device\.alias \|\| device\.hostname\)/);
|
assert.match(panel, /const hasName = Boolean\(device\.alias \|\| device\.hostname\)/);
|
||||||
assert.match(panel, /client-device-alias-trigger[\s\S]*client-device-name-separator[\s\S]*client-device-ip/);
|
assert.match(panel, /client-device-alias-trigger[\s\S]*client-device-name-separator[\s\S]*client-device-ip/);
|
||||||
assert.match(panel, /onClick=\{\(\) => startEditing\(device\)\}/);
|
assert.match(panel, /onClick=\{\(\) => startEditing\(device\)\}/);
|
||||||
assert.match(panel, /\{!hasName && <span className="client-device-edit-wrap/);
|
assert.match(panel, /<span className="client-device-edit-wrap client-tooltip-anchor">/);
|
||||||
|
assert.doesNotMatch(panel, /\{!hasName && <span className="client-device-edit-wrap/);
|
||||||
|
assert.match(panel, /pencilAnimationId === device\.id \? ' is-writing'/);
|
||||||
|
assert.match(panel, /onAnimationEnd=\{\(\) => setPencilAnimationId/);
|
||||||
assert.match(panel, /COPY_FEEDBACK_MS = 800/);
|
assert.match(panel, /COPY_FEEDBACK_MS = 800/);
|
||||||
assert.match(panel, /IP скопирован/);
|
assert.match(panel, /IP скопирован/);
|
||||||
assert.doesNotMatch(panel, /client-device-name-feedback|client-device-name-primary|is-address-only/);
|
assert.doesNotMatch(panel, /client-device-name-feedback|client-device-name-primary|is-address-only/);
|
||||||
@@ -110,7 +117,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font-size: 10px/);
|
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font-size: 10px/);
|
||||||
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
|
assert.match(styles, /\.client-device-name-separator \{[\s\S]*color: var\(--client-muted\)/);
|
||||||
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
|
assert.match(styles, /\.client-device-ip\.is-copied \{[\s\S]*client-device-ip-copy 800ms/);
|
||||||
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding-top: 11px/);
|
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 27px 0 0/);
|
||||||
assert.match(styles, /\.client-device-last-seen \{[^}]*height: 10px[\s\S]*align-items: center/);
|
assert.match(styles, /\.client-device-last-seen \{[^}]*height: 10px[\s\S]*align-items: center/);
|
||||||
assert.match(styles, /\.client-device-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
|
assert.match(styles, /\.client-device-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
|
||||||
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
|
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
|
||||||
@@ -119,7 +126,9 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-pin-wrap\.client-tooltip-anchor:hover > \.client-tooltip[\s\S]*translate\(0, 0\)/);
|
assert.match(styles, /\.client-device-pin-wrap\.client-tooltip-anchor:hover > \.client-tooltip[\s\S]*translate\(0, 0\)/);
|
||||||
assert.match(styles, /\.client-device-pin:hover:not\(:disabled\) svg[\s\S]*translateY\(-2px\) rotate\(-12deg\)/);
|
assert.match(styles, /\.client-device-pin:hover:not\(:disabled\) svg[\s\S]*translateY\(-2px\) rotate\(-12deg\)/);
|
||||||
assert.match(styles, /\.client-device-pin\[aria-pressed="true"\] svg/);
|
assert.match(styles, /\.client-device-pin\[aria-pressed="true"\] svg/);
|
||||||
assert.match(styles, /\.client-device-edit:hover svg[\s\S]*translate\(1px, -1px\) rotate\(-4deg\)/);
|
assert.match(styles, /\.client-device-edit-wrap \{[\s\S]*position: absolute;[\s\S]*right: 0;[\s\S]*bottom: 0/);
|
||||||
|
assert.match(styles, /\.client-device-edit\.is-writing svg \{[\s\S]*client-device-pencil-write 620ms/);
|
||||||
|
assert.match(styles, /@keyframes client-device-pencil-write[\s\S]*0%, 100%[\s\S]*translate\(1px, -1px\) rotate\(-5deg\)/);
|
||||||
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
|
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
|
||||||
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
|
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
|
||||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-ip[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value/);
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-ip[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value/);
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ test('connectivity diagnostics render stable compact tables before the first run
|
|||||||
assert.match(diagnostics, /Добавить свой сервис/);
|
assert.match(diagnostics, /Добавить свой сервис/);
|
||||||
assert.match(diagnostics, /client-diagnostics-refresh/);
|
assert.match(diagnostics, /client-diagnostics-refresh/);
|
||||||
assert.doesNotMatch(diagnostics, /Проверить ещё раз|client-diagnostics-empty|client-diagnostics-run/);
|
assert.doesNotMatch(diagnostics, /Проверить ещё раз|client-diagnostics-empty|client-diagnostics-run/);
|
||||||
assert.match(rule('.client-diagnostics-feedback'), /min-height:\s*34px/);
|
assert.match(diagnostics, /\{\(error \|\| result\) && <div className="client-diagnostics-feedback"/);
|
||||||
|
assert.doesNotMatch(diagnostics, /PathDetails|client-diagnostics-details|Технические детали/);
|
||||||
|
assert.doesNotMatch(rule('.client-diagnostics-feedback'), /min-height:/);
|
||||||
assert.match(rule('.client-diagnostics-table'), /table-layout:\s*fixed/);
|
assert.match(rule('.client-diagnostics-table'), /table-layout:\s*fixed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user