Refine device traffic chart collapse animation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.21.7',
|
macClient: '0.21.8',
|
||||||
gatewayClient: '0.22.2',
|
gatewayClient: '0.22.3',
|
||||||
gatewayBackend: '0.22.5',
|
gatewayBackend: '0.22.5',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -367,7 +367,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
<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 collapsing = Boolean(pinCollapses[device.id]);
|
||||||
const displayPinned = device.pinned || collapsing;
|
const expanded = device.pinned && !collapsing;
|
||||||
|
const showDetails = 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;
|
||||||
@@ -409,7 +410,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}${displayPinned ? ' is-pinned' : ''}${collapsing ? ' is-collapsing' : ''}`}
|
className={`client-device is-${device.status}${expanded ? ' 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">
|
||||||
@@ -528,7 +529,7 @@ 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={displayPinned}
|
pinned={showDetails}
|
||||||
collapsing={collapsing}
|
collapsing={collapsing}
|
||||||
onCollapseEnd={() => finishPinCollapse(device.id)}
|
onCollapseEnd={() => finishPinCollapse(device.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export function TrafficChart({
|
|||||||
'--traffic-chart-mid': `${(100 + TRAFFIC_CHART_HEADROOM) / 2}%`,
|
'--traffic-chart-mid': `${(100 + TRAFFIC_CHART_HEADROOM) / 2}%`,
|
||||||
} as CSSProperties}
|
} as CSSProperties}
|
||||||
onAnimationEnd={(event: AnimationEvent<HTMLSpanElement>) => {
|
onAnimationEnd={(event: AnimationEvent<HTMLSpanElement>) => {
|
||||||
if (collapsing && event.animationName === 'client-device-chart-collapse') onCollapseEnd?.();
|
if (collapsing && event.animationName === 'client-device-traffic-plot-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">
|
||||||
|
|||||||
@@ -670,13 +670,40 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device.is-pinned .client-device-traffic-chart {
|
.client-device.is-pinned .client-device-traffic-plot {
|
||||||
transform-origin: bottom center;
|
transform-origin: top center;
|
||||||
animation: client-device-chart-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-device-traffic-plot-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device.is-pinned .client-device-traffic-chart.is-collapsing {
|
.client-device.is-collapsing .client-device-traffic-plot {
|
||||||
animation-name: client-device-chart-collapse;
|
transform-origin: top center;
|
||||||
|
animation: client-device-traffic-plot-collapse 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device.is-pinned .client-device-traffic-axis,
|
||||||
|
.client-device.is-pinned .client-device-traffic-grid {
|
||||||
|
animation: client-device-traffic-detail-in 360ms 80ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device.is-collapsing .client-device-traffic-axis {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 34px;
|
||||||
|
height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device.is-collapsing .client-device-traffic-axis,
|
||||||
|
.client-device.is-collapsing .client-device-traffic-grid {
|
||||||
|
animation: client-device-traffic-detail-out 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device.is-pinned .client-device-traffic-time {
|
||||||
|
animation: client-device-traffic-time-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device.is-collapsing .client-device-traffic-time {
|
||||||
|
animation: client-device-traffic-time-collapse 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-axis {
|
.client-device-traffic-axis {
|
||||||
@@ -861,30 +888,34 @@
|
|||||||
from { opacity: 0; filter: blur(3px); }
|
from { opacity: 0; filter: blur(3px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-device-chart-expand {
|
@keyframes client-device-traffic-plot-expand {
|
||||||
from {
|
from { transform: scaleY(0.3448275862); }
|
||||||
opacity: 0.72;
|
to { transform: scaleY(1); }
|
||||||
clip-path: inset(calc(100% - 34px) 0 0);
|
|
||||||
transform: scaleY(0.48);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
clip-path: inset(0);
|
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes client-device-chart-collapse {
|
@keyframes client-device-traffic-plot-collapse {
|
||||||
from {
|
from { transform: scaleY(2.9); }
|
||||||
opacity: 1;
|
to { transform: scaleY(1); }
|
||||||
clip-path: inset(0);
|
}
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
@keyframes client-device-traffic-time-expand {
|
||||||
to {
|
from { transform: translateY(-38px); }
|
||||||
opacity: 0.72;
|
to { transform: translateY(0); }
|
||||||
clip-path: inset(calc(100% - 34px) 0 0);
|
}
|
||||||
transform: scaleY(0.48);
|
|
||||||
}
|
@keyframes client-device-traffic-time-collapse {
|
||||||
|
from { transform: translateY(38px); }
|
||||||
|
to { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-device-traffic-detail-in {
|
||||||
|
from { opacity: 0; transform: translateY(5px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-device-traffic-detail-out {
|
||||||
|
from { opacity: 1; transform: translateY(0); }
|
||||||
|
to { opacity: 0; transform: translateY(5px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-policy-wrap {
|
.client-device-policy-wrap {
|
||||||
|
|||||||
@@ -75,6 +75,10 @@
|
|||||||
.client-device-traffic-value > span,
|
.client-device-traffic-value > span,
|
||||||
.client-device-traffic-breakdown,
|
.client-device-traffic-breakdown,
|
||||||
.client-device-traffic-breakdown > span,
|
.client-device-traffic-breakdown > span,
|
||||||
|
.client-device-traffic-plot,
|
||||||
|
.client-device-traffic-axis,
|
||||||
|
.client-device-traffic-grid,
|
||||||
|
.client-device-traffic-time,
|
||||||
.client-device-traffic-lines,
|
.client-device-traffic-lines,
|
||||||
.client-device-traffic-point-tooltip,
|
.client-device-traffic-point-tooltip,
|
||||||
.client-device-traffic-chart,
|
.client-device-traffic-chart,
|
||||||
|
|||||||
@@ -116,13 +116,15 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
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 \[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.match(panel, /const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = device\.pinned \|\| collapsing[\s\S]*pinned=\{showDetails\}[\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 520ms/);
|
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
|
||||||
|
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
|
||||||
|
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
|
||||||
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\)/);
|
||||||
@@ -139,8 +141,10 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
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.doesNotMatch(styles, /client-device-traffic-shift|client-device-traffic-line-draw/);
|
assert.doesNotMatch(styles, /client-device-traffic-shift|client-device-traffic-line-draw/);
|
||||||
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, /@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-traffic-plot-expand[\s\S]*scaleY\(0\.3448275862\)[\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, /@keyframes client-device-traffic-plot-collapse[\s\S]*scaleY\(2\.9\)[\s\S]*scaleY\(1\)/);
|
||||||
|
assert.match(styles, /@keyframes client-device-traffic-detail-in[\s\S]*opacity: 0[\s\S]*opacity: 1/);
|
||||||
|
assert.match(styles, /@keyframes client-device-traffic-detail-out[\s\S]*opacity: 1[\s\S]*opacity: 0/);
|
||||||
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/);
|
||||||
|
|||||||
@@ -36,24 +36,24 @@ 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: 798,
|
cascadeEdges: 804,
|
||||||
customProperties: 31,
|
customProperties: 31,
|
||||||
declarations: 2900,
|
declarations: 2914,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 47,
|
keyframes: 51,
|
||||||
media: 10,
|
media: 10,
|
||||||
rules: 865,
|
rules: 878,
|
||||||
variableReferences: 336,
|
variableReferences: 336,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: 'f177c44a1d610ba7c907ca3dc892e30e33dde390513139b5607f6a3cfe2d7825',
|
cascadeEdges: '18e5bdda435293145dbb52319f1e842cc8b7cc3e8beb60ec381c26d0410cfe44',
|
||||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||||
declarations: '9839eb3b27e0af3861942950de7fc8f01bd0ecf637a371803b616984ed3a61b7',
|
declarations: '01152af9f22aa7a7654356f81b8a6110488f9d70754b2517d1f6bafeff4b14fd',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
||||||
keyframes: 'ee0752081f8987d4b3ef09c0cdb5a34ebcc73e2a452976c509360bdaa5ce7e5c',
|
keyframes: '0bed7ec3cd3a86ee091cf9b07c081ab2364ac5a17e55e95bc9c4a6430419b019',
|
||||||
ruleDeclarationSequences: '56d2adf0cfd83dc14a798fc79bfed2a026ec658a0989926cb17cf0ad9794ffed',
|
ruleDeclarationSequences: '7ffd551563a8d801bcd5e5f9886b9551940dd9561240f0f8a771a85d44fc19db',
|
||||||
selectors: '09042be79ac63f890fc98c78be92ca62ceca37443e4f4b808423243b06379ed8',
|
selectors: '09962ebb3bc769f74ae785a44f8e032c0a83985ce14ea652135732b5af69042a',
|
||||||
variableReferences: '5988cb6c9b658d380c5b51c04621c0579e1cc5e8abcd764d78a9d42b802252c9',
|
variableReferences: '5988cb6c9b658d380c5b51c04621c0579e1cc5e8abcd764d78a9d42b802252c9',
|
||||||
witnesses: '68d7c48f20f7a8ae2c0a6ddabe743dfb84fd02fba303e3cc9d3b902efd0f8758',
|
witnesses: '68d7c48f20f7a8ae2c0a6ddabe743dfb84fd02fba303e3cc9d3b902efd0f8758',
|
||||||
},
|
},
|
||||||
@@ -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-UzWB2BIJ.css']);
|
assert.deepEqual(assets, ['index-BJnEzGgg.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, 107898);
|
assert.equal(built.byteLength, 109140);
|
||||||
assert.equal(sha256(built), 'df4965f0acfa4cf95e8ffa29c4942ce795d4cb903ad6914d54e61077b6ffd6f9');
|
assert.equal(sha256(built), '9e1f7811080177f366b693a77e92c615aff1a74537b34d597100c9707a8aea95');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user