Animate device pin collapse and traffic chart updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.21.6',
|
macClient: '0.21.7',
|
||||||
gatewayClient: '0.22.1',
|
gatewayClient: '0.22.2',
|
||||||
gatewayBackend: '0.22.5',
|
gatewayBackend: '0.22.5',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ interface TrafficDelta {
|
|||||||
total?: string;
|
total?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PinCollapse {
|
||||||
|
animationDone: boolean;
|
||||||
|
result: 'pending' | 'saved' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
function requestMessage(value: unknown) {
|
function requestMessage(value: unknown) {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
||||||
const message: unknown = Reflect.get(value, 'message');
|
const message: unknown = Reflect.get(value, 'message');
|
||||||
@@ -79,9 +84,9 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
const [copyAnnouncement, setCopyAnnouncement] = useState<{ id: string; message: string } | null>(null);
|
const [copyAnnouncement, setCopyAnnouncement] = useState<{ id: string; message: string } | null>(null);
|
||||||
const [pencilAnimationId, setPencilAnimationId] = useState('');
|
const [pencilAnimationId, setPencilAnimationId] = useState('');
|
||||||
const [trafficDeltas, setTrafficDeltas] = useState<Record<string, TrafficDelta>>({});
|
const [trafficDeltas, setTrafficDeltas] = useState<Record<string, TrafficDelta>>({});
|
||||||
|
const [pinCollapses, setPinCollapses] = useState<Record<string, PinCollapse>>({});
|
||||||
const deviceNodes = useRef(new Map<string, HTMLElement>());
|
const deviceNodes = useRef(new Map<string, HTMLElement>());
|
||||||
const previousPositions = useRef(new Map<string, DOMRect>());
|
const previousPositions = useRef(new Map<string, DOMRect>());
|
||||||
const previousOrder = useRef<string[]>([]);
|
|
||||||
const previousScrollTop = useRef(0);
|
const previousScrollTop = useRef(0);
|
||||||
const movementAnimations = useRef(new Map<string, Animation>());
|
const movementAnimations = useRef(new Map<string, Animation>());
|
||||||
const previousTraffic = useRef(new Map<string, { gateway: bigint; proxy: bigint }>());
|
const previousTraffic = useRef(new Map<string, { gateway: bigint; proxy: bigint }>());
|
||||||
@@ -144,7 +149,6 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
previousPositions.current.clear();
|
previousPositions.current.clear();
|
||||||
previousOrder.current = [];
|
|
||||||
previousScrollTop.current = 0;
|
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();
|
||||||
@@ -155,12 +159,9 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
movementAnimations.current.get(id)?.cancel();
|
movementAnimations.current.get(id)?.cancel();
|
||||||
positions.set(id, node.getBoundingClientRect());
|
positions.set(id, node.getBoundingClientRect());
|
||||||
}
|
}
|
||||||
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;
|
const currentScrollTop = panelRef.current?.scrollTop || 0;
|
||||||
if (orderChanged && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
if (previousPositions.current.size > 0
|
||||||
|
&& !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
|
const deltaY = before
|
||||||
@@ -178,9 +179,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousPositions.current = positions;
|
previousPositions.current = positions;
|
||||||
previousOrder.current = order;
|
|
||||||
previousScrollTop.current = currentScrollTop;
|
previousScrollTop.current = currentScrollTop;
|
||||||
}, [devices, open, panelRef]);
|
}, [devices, open, panelRef, pinCollapses]);
|
||||||
|
|
||||||
async function saveAlias(device: Device) {
|
async function saveAlias(device: Device) {
|
||||||
const nextAlias = alias.trim();
|
const nextAlias = alias.trim();
|
||||||
@@ -233,6 +233,39 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
setAlias(value);
|
setAlias(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function togglePin(device: Device) {
|
||||||
|
if (!device.pinned || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
await updateDevice(device, { pinned: !device.pinned });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPinCollapses((current) => ({
|
||||||
|
...current,
|
||||||
|
[device.id]: { animationDone: false, result: 'pending' },
|
||||||
|
}));
|
||||||
|
const saved = await updateDevice(device, { pinned: false });
|
||||||
|
setPinCollapses((current) => {
|
||||||
|
const phase = current[device.id];
|
||||||
|
if (!phase) return current;
|
||||||
|
if (!phase.animationDone) {
|
||||||
|
return { ...current, [device.id]: { ...phase, result: saved ? 'saved' : 'failed' } };
|
||||||
|
}
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[device.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishPinCollapse(deviceId: string) {
|
||||||
|
setPinCollapses((current) => {
|
||||||
|
const phase = current[deviceId];
|
||||||
|
if (!phase) return current;
|
||||||
|
if (phase.result === 'pending') return { ...current, [deviceId]: { ...phase, animationDone: true } };
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[deviceId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
@@ -333,6 +366,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="client-devices-list" aria-live="polite" aria-busy={status === 'loading' || status === 'refreshing'}>
|
<div className="client-devices-list" aria-live="polite" aria-busy={status === 'loading' || status === 'refreshing'}>
|
||||||
{devices.map((device) => {
|
{devices.map((device) => {
|
||||||
|
const collapsing = Boolean(pinCollapses[device.id]);
|
||||||
|
const displayPinned = device.pinned || collapsing;
|
||||||
const hasName = Boolean(device.alias || device.hostname);
|
const hasName = Boolean(device.alias || device.hostname);
|
||||||
const title = device.alias || device.hostname || device.ip || 'Неизвестное устройство';
|
const title = device.alias || device.hostname || device.ip || 'Неизвестное устройство';
|
||||||
const editing = editingId === device.id;
|
const editing = editingId === device.id;
|
||||||
@@ -374,7 +409,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
if (node) deviceNodes.current.set(device.id, node);
|
if (node) deviceNodes.current.set(device.id, node);
|
||||||
else deviceNodes.current.delete(device.id);
|
else deviceNodes.current.delete(device.id);
|
||||||
}}
|
}}
|
||||||
className={`client-device is-${device.status}${device.pinned ? ' is-pinned' : ''}`}
|
className={`client-device is-${device.status}${displayPinned ? ' is-pinned' : ''}${collapsing ? ' is-collapsing' : ''}`}
|
||||||
key={device.id}
|
key={device.id}
|
||||||
>
|
>
|
||||||
<span className="client-device-pin-wrap client-tooltip-anchor">
|
<span className="client-device-pin-wrap client-tooltip-anchor">
|
||||||
@@ -384,7 +419,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
aria-pressed={device.pinned}
|
aria-pressed={device.pinned}
|
||||||
aria-label={device.pinned ? `Открепить ${title}` : `Закрепить ${title}`}
|
aria-label={device.pinned ? `Открепить ${title}` : `Закрепить ${title}`}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
onClick={() => updateDevice(device, { pinned: !device.pinned })}
|
onClick={() => togglePin(device)}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path d="M9 3h6l-1 5 3 3v2H7v-2l3-3-1-5ZM12 13v8" />
|
<path d="M9 3h6l-1 5 3 3v2H7v-2l3-3-1-5ZM12 13v8" />
|
||||||
@@ -493,7 +528,9 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
scale={trafficScale}
|
scale={trafficScale}
|
||||||
capacity={snapshot?.trafficHistoryCapacity || device.trafficHistory?.length || 1}
|
capacity={snapshot?.trafficHistoryCapacity || device.trafficHistory?.length || 1}
|
||||||
routeLabel={device.appliedPolicy === 'direct' ? 'Напрямую' : 'Gateway'}
|
routeLabel={device.appliedPolicy === 'direct' ? 'Напрямую' : 'Gateway'}
|
||||||
pinned={device.pinned}
|
pinned={displayPinned}
|
||||||
|
collapsing={collapsing}
|
||||||
|
onCollapseEnd={() => finishPinCollapse(device.id)}
|
||||||
/>
|
/>
|
||||||
</article>;
|
</article>;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import React, { useLayoutEffect, useRef, useState, type CSSProperties, type PointerEvent } from 'react';
|
import React, {
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type AnimationEvent,
|
||||||
|
type CSSProperties,
|
||||||
|
type PointerEvent,
|
||||||
|
} from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
byteString,
|
byteString,
|
||||||
@@ -41,6 +48,26 @@ function smoothTrafficPath(points: ChartPoint[], valueKey: 'gatewayY' | 'proxyY'
|
|||||||
}, `M ${points[0].x},${points[0][valueKey]}`);
|
}, `M ${points[0].x},${points[0][valueKey]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trafficPathAnimationSource(points: ChartPoint[], previousPoints: ChartPoint[]) {
|
||||||
|
const previousByTime = new Map(previousPoints.map((point) => [point.sample.observedAt, point]));
|
||||||
|
let anchor: ChartPoint | undefined;
|
||||||
|
return points.map((point) => {
|
||||||
|
const exact = previousByTime.get(point.sample.observedAt);
|
||||||
|
if (exact) anchor = exact;
|
||||||
|
const source = exact || anchor;
|
||||||
|
return source ? {
|
||||||
|
...point,
|
||||||
|
x: source.x,
|
||||||
|
gatewayY: source.gatewayY,
|
||||||
|
proxyY: source.proxyY,
|
||||||
|
} : {
|
||||||
|
...point,
|
||||||
|
gatewayY: 100,
|
||||||
|
proxyY: 100,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function trafficSeriesMax(values: Array<{ gateway: bigint; proxy: bigint }>) {
|
function trafficSeriesMax(values: Array<{ gateway: bigint; proxy: bigint }>) {
|
||||||
return values.reduce((largest, { gateway, proxy }) => {
|
return values.reduce((largest, { gateway, proxy }) => {
|
||||||
return gateway > largest
|
return gateway > largest
|
||||||
@@ -59,6 +86,8 @@ export function TrafficChart({
|
|||||||
capacity,
|
capacity,
|
||||||
routeLabel,
|
routeLabel,
|
||||||
pinned = true,
|
pinned = true,
|
||||||
|
collapsing = false,
|
||||||
|
onCollapseEnd,
|
||||||
series = 'routes',
|
series = 'routes',
|
||||||
}: {
|
}: {
|
||||||
samples: TrafficSample[];
|
samples: TrafficSample[];
|
||||||
@@ -66,11 +95,12 @@ export function TrafficChart({
|
|||||||
capacity: number;
|
capacity: number;
|
||||||
routeLabel: string;
|
routeLabel: string;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
|
collapsing?: boolean;
|
||||||
|
onCollapseEnd?: () => void;
|
||||||
series?: 'routes' | 'speed';
|
series?: 'routes' | 'speed';
|
||||||
}) {
|
}) {
|
||||||
const [hovered, setHovered] = useState<HoveredPoint | null>(null);
|
const [hovered, setHovered] = useState<HoveredPoint | null>(null);
|
||||||
const previousPoints = useRef<ChartPoint[]>([]);
|
const previousPoints = useRef<ChartPoint[]>([]);
|
||||||
const previousScale = useRef<TrafficScale>(scale);
|
|
||||||
const previousSeries = useRef(series);
|
const previousSeries = useRef(series);
|
||||||
const speedAvailable = samples.length > 1 && samples.every((sample) => (
|
const speedAvailable = samples.length > 1 && samples.every((sample) => (
|
||||||
typeof sample.downloadBytes === 'string' && typeof sample.uploadBytes === 'string'
|
typeof sample.downloadBytes === 'string' && typeof sample.uploadBytes === 'string'
|
||||||
@@ -108,17 +138,22 @@ export function TrafficChart({
|
|||||||
const penultimate = points.at(-2);
|
const penultimate = points.at(-2);
|
||||||
const newest = points.at(-1);
|
const newest = points.at(-1);
|
||||||
const hasSecondary = points.some(({ proxy }) => proxy > 0n);
|
const hasSecondary = points.some(({ proxy }) => proxy > 0n);
|
||||||
const scaleFrom = previousPoints.current;
|
const motionFrom = previousSeries.current === series ? previousPoints.current : [];
|
||||||
const animateScale = previousScale.current !== scale
|
const previousMotionFrom = trafficPathAnimationSource(previous, motionFrom);
|
||||||
&& previousSeries.current === series
|
const newestMotionFrom = trafficPathAnimationSource(
|
||||||
&& scaleFrom.length === points.length
|
penultimate && newest ? [penultimate, newest] : [],
|
||||||
|
motionFrom,
|
||||||
|
);
|
||||||
|
const motionKey = `${series}-${scale}-${points.map(({ sample, gateway, proxy }) => (
|
||||||
|
`${sample.observedAt}:${gateway}:${proxy}`
|
||||||
|
)).join('|')}`;
|
||||||
|
const animatePaths = points.length > 0
|
||||||
&& !(typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
&& !(typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
previousPoints.current = points;
|
previousPoints.current = points;
|
||||||
previousScale.current = scale;
|
|
||||||
previousSeries.current = series;
|
previousSeries.current = series;
|
||||||
}, [points, scale, series]);
|
}, [points, series]);
|
||||||
|
|
||||||
function trackPointer(event: PointerEvent<HTMLSpanElement>) {
|
function trackPointer(event: PointerEvent<HTMLSpanElement>) {
|
||||||
const bounds = event.currentTarget.getBoundingClientRect();
|
const bounds = event.currentTarget.getBoundingClientRect();
|
||||||
@@ -142,14 +177,14 @@ export function TrafficChart({
|
|||||||
>
|
>
|
||||||
<time dateTime={hovered.sample.observedAt}>{chartTime(hovered.sample.observedAt)}</time>
|
<time dateTime={hovered.sample.observedAt}>{chartTime(hovered.sample.observedAt)}</time>
|
||||||
{series === 'speed' ? <>
|
{series === 'speed' ? <>
|
||||||
<strong>↓ Download {formatRate(hovered.gateway)}</strong>
|
<strong className="is-download">↓ Download {formatRate(hovered.gateway)}</strong>
|
||||||
<span className="is-upload">↑ Upload {formatRate(hovered.proxy)}</span>
|
<span className="is-upload">↑ Upload {formatRate(hovered.proxy)}</span>
|
||||||
<span>За интервал {formatByteString(byteString(hovered.sample.gatewayBytes) + byteString(hovered.sample.proxyBytes))}</span>
|
<span className="is-interval">За интервал {formatByteString(byteString(hovered.sample.gatewayBytes) + byteString(hovered.sample.proxyBytes))}</span>
|
||||||
<span>{routeLabel} {formatByteString(hovered.sample.gatewayBytes)}</span>
|
<span className={routeLabel === 'Gateway' ? 'is-gateway' : 'is-direct'}>{routeLabel} {formatByteString(hovered.sample.gatewayBytes)}</span>
|
||||||
{byteString(hovered.sample.proxyBytes) > 0n && <span className="is-proxy">Proxy {formatByteString(hovered.sample.proxyBytes)}</span>}
|
{byteString(hovered.sample.proxyBytes) > 0n && <span className="is-proxy">Proxy {formatByteString(hovered.sample.proxyBytes)}</span>}
|
||||||
</> : <>
|
</> : <>
|
||||||
<strong>Всего {formatByteString(hovered.gateway + hovered.proxy)}</strong>
|
<strong className="is-total">Всего {formatByteString(hovered.gateway + hovered.proxy)}</strong>
|
||||||
<span>{routeLabel} {formatByteString(hovered.gateway)}</span>
|
<span className={routeLabel === 'Gateway' ? 'is-gateway' : 'is-direct'}>{routeLabel} {formatByteString(hovered.gateway)}</span>
|
||||||
{hovered.proxy > 0n && <span className="is-proxy">Proxy {formatByteString(hovered.proxy)}</span>}
|
{hovered.proxy > 0n && <span className="is-proxy">Proxy {formatByteString(hovered.proxy)}</span>}
|
||||||
</>}
|
</>}
|
||||||
</span>,
|
</span>,
|
||||||
@@ -157,13 +192,16 @@ export function TrafficChart({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return <span
|
return <span
|
||||||
className="client-device-traffic-chart"
|
className={`client-device-traffic-chart${collapsing ? ' is-collapsing' : ''}`}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`${series === 'speed' ? 'История скорости' : 'История трафика'}, шкала ${scale === 'log' ? 'логарифмическая' : 'линейная'}, максимум ${series === 'speed' ? formatRate(max) : formatByteString(max)}`}
|
aria-label={`${series === 'speed' ? 'История скорости' : 'История трафика'}, шкала ${scale === 'log' ? 'логарифмическая' : 'линейная'}, максимум ${series === 'speed' ? formatRate(max) : formatByteString(max)}`}
|
||||||
style={{
|
style={{
|
||||||
'--traffic-chart-top': `${TRAFFIC_CHART_HEADROOM}%`,
|
'--traffic-chart-top': `${TRAFFIC_CHART_HEADROOM}%`,
|
||||||
'--traffic-chart-mid': `${(100 + TRAFFIC_CHART_HEADROOM) / 2}%`,
|
'--traffic-chart-mid': `${(100 + TRAFFIC_CHART_HEADROOM) / 2}%`,
|
||||||
} as CSSProperties}
|
} as CSSProperties}
|
||||||
|
onAnimationEnd={(event: AnimationEvent<HTMLSpanElement>) => {
|
||||||
|
if (collapsing && event.animationName === 'client-device-chart-collapse') onCollapseEnd?.();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{pinned && max > 0n && <span className="client-device-traffic-axis" aria-hidden="true">
|
{pinned && max > 0n && <span className="client-device-traffic-axis" aria-hidden="true">
|
||||||
<span className="is-max">{series === 'speed' ? formatRate(max) : formatByteString(max)}</span>
|
<span className="is-max">{series === 'speed' ? formatRate(max) : formatByteString(max)}</span>
|
||||||
@@ -177,20 +215,22 @@ export function TrafficChart({
|
|||||||
<line x1="0" x2="100" y1={(100 + TRAFFIC_CHART_HEADROOM) / 2} y2={(100 + TRAFFIC_CHART_HEADROOM) / 2} />
|
<line x1="0" x2="100" y1={(100 + TRAFFIC_CHART_HEADROOM) / 2} y2={(100 + TRAFFIC_CHART_HEADROOM) / 2} />
|
||||||
<line x1="0" x2="100" y1="100" y2="100" />
|
<line x1="0" x2="100" y1="100" y2="100" />
|
||||||
</g>}
|
</g>}
|
||||||
<g className="client-device-traffic-lines" style={{ '--sample-count': Math.max(1, capacity) } as CSSProperties}>
|
<g className="client-device-traffic-lines">
|
||||||
{previous.length > 0 && <path className={series === 'speed' ? 'is-download' : 'is-gateway'} d={smoothTrafficPath(previous, 'gatewayY')}>
|
{previous.length > 0 && <path className={series === 'speed' ? 'is-download' : 'is-gateway'} d={smoothTrafficPath(previous, 'gatewayY')}>
|
||||||
{animateScale && <animate key={`gateway-${series}-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(0, -1), 'gatewayY')} to={smoothTrafficPath(previous, 'gatewayY')} dur="520ms" calcMode="spline" keyTimes="0;1" keySplines="0.16 1 0.3 1" fill="freeze" />}
|
{animatePaths && <animate key={`gateway-${motionKey}`} attributeName="d" from={smoothTrafficPath(previousMotionFrom, 'gatewayY')} to={smoothTrafficPath(previous, 'gatewayY')} dur="520ms" calcMode="spline" keyTimes="0;1" keySplines="0.16 1 0.3 1" fill="freeze" />}
|
||||||
</path>}
|
</path>}
|
||||||
{hasSecondary && previous.length > 0 && <path className={series === 'speed' ? 'is-upload' : 'is-proxy'} d={smoothTrafficPath(previous, 'proxyY')}>
|
{hasSecondary && previous.length > 0 && <path className={series === 'speed' ? 'is-upload' : 'is-proxy'} d={smoothTrafficPath(previous, 'proxyY')}>
|
||||||
{animateScale && <animate key={`proxy-${series}-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(0, -1), 'proxyY')} to={smoothTrafficPath(previous, 'proxyY')} dur="520ms" calcMode="spline" keyTimes="0;1" keySplines="0.16 1 0.3 1" fill="freeze" />}
|
{animatePaths && <animate key={`proxy-${motionKey}`} attributeName="d" from={smoothTrafficPath(previousMotionFrom, 'proxyY')} to={smoothTrafficPath(previous, 'proxyY')} dur="520ms" calcMode="spline" keyTimes="0;1" keySplines="0.16 1 0.3 1" fill="freeze" />}
|
||||||
</path>}
|
</path>}
|
||||||
{penultimate && newest && <path className={`${series === 'speed' ? 'is-download' : 'is-gateway'} is-new`} pathLength="1" d={smoothTrafficPath([penultimate, newest], 'gatewayY')}>
|
{penultimate && newest && <path className={series === 'speed' ? 'is-download' : 'is-gateway'} d={smoothTrafficPath([penultimate, newest], 'gatewayY')}>
|
||||||
{animateScale && <animate key={`gateway-new-${series}-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(-2), 'gatewayY')} to={smoothTrafficPath([penultimate, newest], 'gatewayY')} dur="520ms" fill="freeze" />}
|
{animatePaths && <animate key={`gateway-new-${motionKey}`} attributeName="d" from={smoothTrafficPath(newestMotionFrom, 'gatewayY')} to={smoothTrafficPath([penultimate, newest], 'gatewayY')} dur="520ms" calcMode="spline" keyTimes="0;1" keySplines="0.16 1 0.3 1" fill="freeze" />}
|
||||||
</path>}
|
</path>}
|
||||||
{hasSecondary && penultimate && newest && <path className={`${series === 'speed' ? 'is-upload' : 'is-proxy'} is-new`} pathLength="1" d={smoothTrafficPath([penultimate, newest], 'proxyY')}>
|
{hasSecondary && penultimate && newest && <path className={series === 'speed' ? 'is-upload' : 'is-proxy'} d={smoothTrafficPath([penultimate, newest], 'proxyY')}>
|
||||||
{animateScale && <animate key={`proxy-new-${series}-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(-2), 'proxyY')} to={smoothTrafficPath([penultimate, newest], 'proxyY')} dur="520ms" fill="freeze" />}
|
{animatePaths && <animate key={`proxy-new-${motionKey}`} attributeName="d" from={smoothTrafficPath(newestMotionFrom, 'proxyY')} to={smoothTrafficPath([penultimate, newest], 'proxyY')} dur="520ms" calcMode="spline" keyTimes="0;1" keySplines="0.16 1 0.3 1" fill="freeze" />}
|
||||||
</path>}
|
</path>}
|
||||||
{!penultimate && newest && <line className={`${series === 'speed' ? 'is-download' : 'is-gateway'} is-point`} x1={newest.x} x2={newest.x} y1={newest.gatewayY} y2={newest.gatewayY} />}
|
{!penultimate && newest && <line className={`${series === 'speed' ? 'is-download' : 'is-gateway'} is-point`} x1={newest.x} x2={newest.x} y1={newest.gatewayY} y2={newest.gatewayY}>
|
||||||
|
{animatePaths && <animate key={`point-${motionKey}`} attributeName="opacity" from="0" to="1" dur="220ms" fill="freeze" />}
|
||||||
|
</line>}
|
||||||
</g>
|
</g>
|
||||||
{hovered && <g className="client-device-traffic-cursor">
|
{hovered && <g className="client-device-traffic-cursor">
|
||||||
<line className="is-guide" x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
|
<line className="is-guide" x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
|
||||||
|
|||||||
@@ -672,7 +672,11 @@
|
|||||||
|
|
||||||
.client-device.is-pinned .client-device-traffic-chart {
|
.client-device.is-pinned .client-device-traffic-chart {
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
animation: client-device-chart-expand 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-device-chart-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device.is-pinned .client-device-traffic-chart.is-collapsing {
|
||||||
|
animation-name: client-device-chart-collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-axis {
|
.client-device-traffic-axis {
|
||||||
@@ -727,11 +731,6 @@
|
|||||||
vector-effect: non-scaling-stroke;
|
vector-effect: non-scaling-stroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines {
|
|
||||||
transform-box: view-box;
|
|
||||||
animation: client-device-traffic-shift 560ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-traffic-lines path,
|
.client-device-traffic-lines path,
|
||||||
.client-device-traffic-lines line {
|
.client-device-traffic-lines line {
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -743,30 +742,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines .is-proxy {
|
.client-device-traffic-lines .is-proxy {
|
||||||
stroke: var(--client-accent);
|
stroke: var(--harbor-word);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-lines .is-gateway {
|
||||||
|
stroke: var(--harbor-gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines .is-download {
|
.client-device-traffic-lines .is-download {
|
||||||
stroke: var(--client-accent);
|
stroke: var(--harbor-connect);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines .is-upload {
|
.client-device-traffic-lines .is-upload {
|
||||||
stroke: color-mix(in oklch, var(--client-accent) 42%, var(--client-text));
|
stroke: var(--harbor-gateway);
|
||||||
stroke-dasharray: 5 4;
|
stroke-dasharray: 5 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines path.is-new {
|
|
||||||
stroke-dasharray: 1;
|
|
||||||
stroke-dashoffset: 1;
|
|
||||||
animation: client-device-traffic-line-draw 620ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-traffic-lines path.is-new.is-upload {
|
|
||||||
stroke-dasharray: 0.08 0.05;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-traffic-lines .is-point {
|
.client-device-traffic-lines .is-point {
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
@@ -787,15 +778,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-cursor .is-point.is-proxy {
|
.client-device-traffic-cursor .is-point.is-proxy {
|
||||||
stroke: var(--client-accent);
|
stroke: var(--harbor-word);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-cursor .is-point.is-gateway {
|
||||||
|
stroke: var(--harbor-gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-cursor .is-point.is-download {
|
.client-device-traffic-cursor .is-point.is-download {
|
||||||
stroke: var(--client-accent);
|
stroke: var(--harbor-connect);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-cursor .is-point.is-upload {
|
.client-device-traffic-cursor .is-point.is-upload {
|
||||||
stroke: color-mix(in oklch, var(--client-accent) 42%, var(--client-text));
|
stroke: var(--harbor-gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-time {
|
.client-device-traffic-time {
|
||||||
@@ -815,12 +810,17 @@
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
padding: 7px 8px;
|
padding: 8px 9px;
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
background: oklch(0.14 0.012 145);
|
background: color-mix(in oklch, oklch(0.14 0.012 145) 92%, transparent);
|
||||||
box-shadow: 0 8px 24px oklch(0.08 0.015 145 / 0.18);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px color-mix(in oklch, var(--client-border) 55%, transparent),
|
||||||
|
0 8px 24px oklch(0.08 0.015 145 / 0.22);
|
||||||
color: oklch(0.92 0.008 145);
|
color: oklch(0.92 0.008 145);
|
||||||
font: 600 9px/1.25 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
font: 600 9px/1.25 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: client-device-traffic-tooltip-in 140ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-device-traffic-tooltip-in 140ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
@@ -837,20 +837,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-point-tooltip .is-proxy {
|
.client-device-traffic-point-tooltip .is-proxy {
|
||||||
color: var(--client-accent);
|
color: oklch(0.78 0.07 232);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-point-tooltip .is-gateway {
|
||||||
|
color: oklch(0.79 0.11 72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-point-tooltip .is-download {
|
||||||
|
color: oklch(0.75 0.1 185);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-point-tooltip .is-upload {
|
.client-device-traffic-point-tooltip .is-upload {
|
||||||
color: color-mix(in oklch, var(--client-accent) 56%, oklch(0.92 0.008 145));
|
color: oklch(0.79 0.11 72);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-device-traffic-shift {
|
.client-device-traffic-point-tooltip .is-interval,
|
||||||
from { opacity: 0.72; transform: translateX(calc(100% / var(--sample-count))); }
|
.client-device-traffic-point-tooltip .is-direct {
|
||||||
to { opacity: 1; transform: translateX(0); }
|
color: oklch(0.72 0.012 145);
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes client-device-traffic-line-draw {
|
|
||||||
to { stroke-dashoffset: 0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-device-traffic-tooltip-in {
|
@keyframes client-device-traffic-tooltip-in {
|
||||||
@@ -870,6 +874,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes client-device-chart-collapse {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
clip-path: inset(0);
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0.72;
|
||||||
|
clip-path: inset(calc(100% - 34px) 0 0);
|
||||||
|
transform: scaleY(0.48);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.client-device-policy-wrap {
|
.client-device-policy-wrap {
|
||||||
grid-column: 4;
|
grid-column: 4;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
@@ -1030,11 +1047,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-speed .is-download {
|
.client-gateway-traffic-speed .is-download {
|
||||||
color: var(--client-accent);
|
color: var(--harbor-connect);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-speed .is-upload {
|
.client-gateway-traffic-speed .is-upload {
|
||||||
color: color-mix(in oklch, var(--client-accent) 48%, var(--client-text));
|
color: var(--harbor-gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-chart {
|
.client-gateway-traffic-chart {
|
||||||
|
|||||||
@@ -118,11 +118,6 @@
|
|||||||
stroke-dashoffset: 0;
|
stroke-dashoffset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines path.is-new {
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-local-rules,
|
.client-local-rules,
|
||||||
.client-local-rules-toggle,
|
.client-local-rules-toggle,
|
||||||
.client-local-rules-toggle svg,
|
.client-local-rules-toggle svg,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(feature, /requestError\(caught\)\.code !== 'STATE_CONFLICT'[\s\S]*listDevices\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*requestDeviceUpdate\(device\.id, patch, latest\.revision\)/);
|
assert.match(feature, /requestError\(caught\)\.code !== 'STATE_CONFLICT'[\s\S]*listDevices\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*requestDeviceUpdate\(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, /previousPositions\.current\.size > 0/);
|
||||||
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
|
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
|
||||||
assert.match(feature, /next\.revision > current\.revision/);
|
assert.match(feature, /next\.revision > current\.revision/);
|
||||||
assert.doesNotMatch(panel, /revision >= current\.revision/);
|
assert.doesNotMatch(panel, /revision >= current\.revision/);
|
||||||
@@ -86,7 +86,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /TrafficChart[\s\S]*samples=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\?\.trafficHistoryCapacity/);
|
assert.match(panel, /TrafficChart[\s\S]*samples=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\?\.trafficHistoryCapacity/);
|
||||||
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
|
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
|
||||||
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
|
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
|
||||||
assert.match(chart, /previousScale\.current !== scale[\s\S]*attributeName="d"[\s\S]*dur="520ms"/);
|
assert.match(chart, /function trafficPathAnimationSource[\s\S]*previousByTime[\s\S]*gatewayY: 100[\s\S]*attributeName="d"[\s\S]*dur="520ms"/);
|
||||||
assert.match(chart, /function smoothTrafficPath[\s\S]*const midX = \(previous\.x \+ point\.x\) \/ 2[\s\S]* C /);
|
assert.match(chart, /function smoothTrafficPath[\s\S]*const midX = \(previous\.x \+ point\.x\) \/ 2[\s\S]* C /);
|
||||||
assert.match(chart, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio: number\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
|
assert.match(chart, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio: number\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
|
||||||
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)/);
|
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)/);
|
||||||
@@ -115,12 +115,14 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /client-drawer client-instructions client-devices/);
|
assert.match(panel, /client-drawer client-instructions client-devices/);
|
||||||
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
||||||
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
||||||
|
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
|
||||||
|
assert.match(panel, /const displayPinned = device\.pinned \|\| collapsing[\s\S]*collapsing=\{collapsing\}[\s\S]*onCollapseEnd=/);
|
||||||
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
|
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
|
||||||
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
|
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
|
||||||
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
|
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
|
||||||
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
|
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
|
||||||
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
|
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
|
||||||
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-chart \{[\s\S]*client-device-chart-expand 620ms/);
|
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-chart \{[\s\S]*client-device-chart-expand 520ms/);
|
||||||
assert.match(styles, /\.client-device-main \{[\s\S]*display: flex;[\s\S]*align-items: center/);
|
assert.match(styles, /\.client-device-main \{[\s\S]*display: flex;[\s\S]*align-items: center/);
|
||||||
assert.match(styles, /\.client-device-traffic-total,[\s\S]*\.client-device-traffic-breakdown > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
|
assert.match(styles, /\.client-device-traffic-total,[\s\S]*\.client-device-traffic-breakdown > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
|
||||||
assert.match(styles, /\.client-device-traffic-breakdown > span \{[\s\S]*translateY\(-12px\)[\s\S]*\.client-device-traffic:hover \.client-device-traffic-breakdown > span,[\s\S]*opacity: 1[\s\S]*translateY\(0\)/);
|
assert.match(styles, /\.client-device-traffic-breakdown > span \{[\s\S]*translateY\(-12px\)[\s\S]*\.client-device-traffic:hover \.client-device-traffic-breakdown > span,[\s\S]*opacity: 1[\s\S]*translateY\(0\)/);
|
||||||
@@ -135,11 +137,10 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-traffic-axis > \.is-max \{[\s\S]*top: var\(--traffic-chart-top\)/);
|
assert.match(styles, /\.client-device-traffic-axis > \.is-max \{[\s\S]*top: var\(--traffic-chart-top\)/);
|
||||||
assert.match(styles, /\.client-device-traffic-axis > \.is-mid \{[\s\S]*top: var\(--traffic-chart-mid\)/);
|
assert.match(styles, /\.client-device-traffic-axis > \.is-mid \{[\s\S]*top: var\(--traffic-chart-mid\)/);
|
||||||
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
|
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
|
||||||
assert.match(styles, /@keyframes client-device-traffic-shift[\s\S]*translateX\(calc\(100% \/ var\(--sample-count\)\)\)[\s\S]*translateX\(0\)/);
|
assert.doesNotMatch(styles, /client-device-traffic-shift|client-device-traffic-line-draw/);
|
||||||
assert.match(styles, /@keyframes client-device-traffic-line-draw[\s\S]*stroke-dashoffset: 0/);
|
|
||||||
assert.match(styles, /\.client-device-traffic-lines \.is-upload \{[\s\S]*stroke-dasharray: 5 4/);
|
assert.match(styles, /\.client-device-traffic-lines \.is-upload \{[\s\S]*stroke-dasharray: 5 4/);
|
||||||
assert.match(styles, /\.client-device-traffic-lines path\.is-new\.is-upload \{[\s\S]*stroke-dashoffset: 0;[\s\S]*animation: none/);
|
|
||||||
assert.match(styles, /@keyframes client-device-chart-expand[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(1\)/);
|
assert.match(styles, /@keyframes client-device-chart-expand[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(1\)/);
|
||||||
|
assert.match(styles, /@keyframes client-device-chart-collapse[\s\S]*clip-path: inset\(0\)[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(0\.48\)/);
|
||||||
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
|
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
|
||||||
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: 700 14px\/1\.2/);
|
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: 700 14px\/1\.2/);
|
||||||
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*client-device-alias-edit-in 360ms/);
|
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*client-device-alias-edit-in 360ms/);
|
||||||
@@ -168,7 +169,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
|
assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
|
||||||
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[\s\S]*\.client-device-traffic-lines path\.is-new \{[\s\S]*stroke-dashoffset: 0;[\s\S]*animation: none/);
|
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[\s\S]*transition: none;[\s\S]*animation: none/);
|
||||||
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
|
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
|
||||||
assert.match(styles, /\.harbor-versions \{[\s\S]*z-index: 40/);
|
assert.match(styles, /\.harbor-versions \{[\s\S]*z-index: 40/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,26 +36,26 @@ const expectedImports = [
|
|||||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||||
const acceptedLedger = {
|
const acceptedLedger = {
|
||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 797,
|
cascadeEdges: 798,
|
||||||
customProperties: 31,
|
customProperties: 31,
|
||||||
declarations: 2900,
|
declarations: 2900,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 48,
|
keyframes: 47,
|
||||||
media: 10,
|
media: 10,
|
||||||
rules: 864,
|
rules: 865,
|
||||||
variableReferences: 339,
|
variableReferences: 336,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '55bd3926b5da1a9ccc6dee5f3eb5cf2c111183e0410edcc085545e5a35c0b229',
|
cascadeEdges: 'f177c44a1d610ba7c907ca3dc892e30e33dde390513139b5607f6a3cfe2d7825',
|
||||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||||
declarations: '67441a07783c586844638db628ab401bbf52bdb1eaf5dbec20e65bebff6faac1',
|
declarations: '9839eb3b27e0af3861942950de7fc8f01bd0ecf637a371803b616984ed3a61b7',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
||||||
keyframes: '853c54c05759d9db27bea891254913e1651b1f601059ad9e3e2baa2c55ef1b2b',
|
keyframes: 'ee0752081f8987d4b3ef09c0cdb5a34ebcc73e2a452976c509360bdaa5ce7e5c',
|
||||||
ruleDeclarationSequences: '75f73d80e2c00e0575974b3cfdef3d27cd509e598f09250223e813f107e0169e',
|
ruleDeclarationSequences: '56d2adf0cfd83dc14a798fc79bfed2a026ec658a0989926cb17cf0ad9794ffed',
|
||||||
selectors: '7e87d3d05fa525ad2414fbd58bd128a1b30be5c4b01a27151c03c9c2692d6384',
|
selectors: '09042be79ac63f890fc98c78be92ca62ceca37443e4f4b808423243b06379ed8',
|
||||||
variableReferences: 'da4298e47c6249c3cfa12168d3670b0ec9c92c0fca7033ba968ea70598753a65',
|
variableReferences: '5988cb6c9b658d380c5b51c04621c0579e1cc5e8abcd764d78a9d42b802252c9',
|
||||||
witnesses: '855149a346b32cf84595b8aabf3acef047d7366ee87aea79ab66165042e3a001',
|
witnesses: '68d7c48f20f7a8ae2c0a6ddabe743dfb84fd02fba303e3cc9d3b902efd0f8758',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
|
|||||||
|
|
||||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||||
const witnesses = readStyleWitnesses(root);
|
const witnesses = readStyleWitnesses(root);
|
||||||
assert.equal(witnesses.length, 707);
|
assert.equal(witnesses.length, 709);
|
||||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||||
@@ -293,8 +293,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
|||||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||||
|
|
||||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||||
assert.deepEqual(assets, ['index-CKZGCKzI.css']);
|
assert.deepEqual(assets, ['index-UzWB2BIJ.css']);
|
||||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||||
assert.equal(built.byteLength, 107832);
|
assert.equal(built.byteLength, 107898);
|
||||||
assert.equal(sha256(built), 'fee93089544bccf45203d53329d54ae10092e3bd93f0cd9b83dccda2202406ef');
|
assert.equal(sha256(built), 'df4965f0acfa4cf95e8ffa29c4942ce795d4cb903ad6914d54e61077b6ffd6f9');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user