From ffd899033e03daaa64d76f141385e51b63a2ec09 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Fri, 7 Aug 2026 19:43:56 +0300 Subject: [PATCH] Refine device inventory layout and traffic chart styling --- src/shared/versions.js | 4 +- src/web/components/DevicesPanel.jsx | 2 +- src/web/styles.css | 55 +++++++++++++++------- test/web/device-inventory-contract.test.js | 15 +++--- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/shared/versions.js b/src/shared/versions.js index a9c9ffc..f84cdcc 100644 --- a/src/shared/versions.js +++ b/src/shared/versions.js @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.14.1', - gatewayClient: '0.15.1', + macClient: '0.14.2', + gatewayClient: '0.15.2', gatewayBackend: '0.15.0', }); diff --git a/src/web/components/DevicesPanel.jsx b/src/web/components/DevicesPanel.jsx index 1b43166..5ffb23d 100644 --- a/src/web/components/DevicesPanel.jsx +++ b/src/web/components/DevicesPanel.jsx @@ -479,7 +479,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) { dateTime={device.lastSeenAt} aria-label={`${online ? 'В сети' : 'Не в сети'}. Последний контакт: ${seen.tooltip}`} > - {online ? 'В сети' : } + {online ? 'В сети' : } diff --git a/src/web/styles.css b/src/web/styles.css index 948d6d1..5ad1c7b 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -936,28 +936,30 @@ p { grid-column: 2; grid-row: 1; height: 34px; + position: relative; min-width: 0; display: flex; - align-items: center; + align-items: flex-end; gap: 8px; + padding-top: 11px; } .client-device-main > h3 { - height: 32px; + height: 23px; flex: 0 1 auto; min-width: 0; display: flex; align-items: center; overflow: hidden; margin: 0; - font-size: 15px; + font-size: 14px; letter-spacing: -0.03em; text-overflow: ellipsis; white-space: nowrap; } .client-device-name { - height: 32px; + height: 23px; width: 100%; min-width: 0; display: grid; @@ -967,12 +969,16 @@ p { border: 0; background: transparent; color: var(--client-text); - font: 700 15px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace; + font: 700 14px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace; letter-spacing: -0.03em; text-align: left; cursor: copy; } +.client-device-name-ip { + font-size: 12px; +} + .client-device-name > span { grid-area: 1 / 1; overflow: hidden; @@ -1051,6 +1057,8 @@ p { .client-device-edit-wrap { flex: 0 0 auto; + height: 23px; + align-self: flex-end; opacity: 0.34; transition: opacity 180ms ease; } @@ -1076,6 +1084,11 @@ p { color: var(--client-muted); } +.client-device-edit { + width: 26px; + height: 23px; +} + .client-device-edit svg, .client-device-pin svg { width: 17px; @@ -1120,14 +1133,16 @@ p { } .client-device-last-seen { - height: 32px; - flex: 0 1 auto; + position: absolute; + top: 0; + left: 0; + height: 10px; min-width: 0; display: flex; align-items: center; - max-width: 116px; + max-width: 150px; overflow: hidden; - font-size: 10px; + font-size: 8.5px; text-overflow: ellipsis; color: var(--client-text); cursor: default; @@ -1163,7 +1178,7 @@ p { .client-text-morph-anchor, .client-text-morph-value { grid-area: 1 / 1; - justify-self: end; + justify-self: start; white-space: nowrap; } @@ -1175,7 +1190,7 @@ p { will-change: opacity, filter, transform; filter: blur(0); transform: translateY(0) scale(1); - transform-origin: right center; + transform-origin: left center; transition: opacity 360ms ease, filter 480ms cubic-bezier(0.16, 1, 0.3, 1), transform 480ms cubic-bezier(0.16, 1, 0.3, 1); } @@ -1211,7 +1226,7 @@ p { width: 112px; height: 34px; position: relative; - z-index: 3; + z-index: 6; color: var(--client-text); font-variant-numeric: tabular-nums; } @@ -1331,7 +1346,7 @@ p { } .client-device-traffic-chart { - grid-column: 2 / 3; + grid-column: 2 / 4; grid-row: 2; height: 42px; position: relative; @@ -1339,6 +1354,10 @@ p { overflow: visible; } +.client-device-traffic-chart:has(.client-device-traffic-bar:hover) { + z-index: 7; +} + .client-device-traffic-chart::after { position: absolute; right: 0; @@ -1355,7 +1374,7 @@ p { display: grid; grid-template-columns: repeat(var(--sample-count), minmax(1px, 1fr)); align-items: flex-end; - gap: 1px; + gap: 0; animation: client-device-traffic-shift 420ms cubic-bezier(0.16, 1, 0.3, 1); } @@ -1365,6 +1384,10 @@ p { cursor: help; } +.client-device-traffic-bar:hover { + z-index: 10; +} + .client-device-traffic-time { position: absolute; right: 2px; @@ -1381,10 +1404,10 @@ p { .client-device-traffic-bar-fill { position: absolute; - inset: 0; + inset: 0 -2px; display: flex; overflow: hidden; - border-radius: 2px 2px 0 0; + border-radius: 3px 3px 0 0; flex-direction: column-reverse; } diff --git a/test/web/device-inventory-contract.test.js b/test/web/device-inventory-contract.test.js index 6bf4cce..513f419 100644 --- a/test/web/device-inventory-contract.test.js +++ b/test/web/device-inventory-contract.test.js @@ -37,7 +37,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer', assert.match(panel, /COPY_FEEDBACK_MS = 5_000/); assert.match(panel, /client-device-name-feedback[\s\S]*client-device-edit-wrap[\s\S]*client-device-last-seen/); assert.doesNotMatch(panel, /client-device-title/); - assert.match(panel, /online \? 'В сети' : /); + assert.match(panel, /online \? 'В сети' : /); assert.match(panel, /aria-label=\{`\$\{online \? 'В сети' : 'Не в сети'\}\. Последний контакт: \$\{seen\.tooltip\}`\}/); assert.doesNotMatch(panel, /client-device-status|STATUS_LABELS/); assert.doesNotMatch(panel, /client-text-morph-goo/); @@ -82,14 +82,17 @@ test('Gateway device inventory uses the existing accessible responsive drawer', 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.doesNotMatch(styles, /\.client-device-traffic-breakdown \{[^}]*background:|\.client-device-traffic-breakdown \{[^}]*box-shadow:/); - assert.match(styles, /\.client-device-traffic-chart \{[\s\S]*grid-column: 2 \/ 3;[\s\S]*grid-row: 2/); + assert.match(styles, /\.client-device-traffic-chart \{[\s\S]*grid-column: 2 \/ 4;[\s\S]*grid-row: 2/); assert.match(styles, /\.client-device-traffic-track \{[\s\S]*client-device-traffic-shift 420ms/); + assert.match(styles, /\.client-device-traffic-track \{[^}]*gap: 0/); + assert.match(styles, /\.client-device-traffic-bar-fill \{[\s\S]*inset: 0 -2px/); assert.match(styles, /\.client-device-traffic-bar:hover \.client-device-traffic-bar-tooltip \{[\s\S]*opacity: 1[\s\S]*visibility: visible/); assert.match(styles, /\.client-device-traffic-bar\.is-edge-left[\s\S]*\.client-device-traffic-bar\.is-edge-right/); assert.match(styles, /@keyframes client-device-traffic-shift[\s\S]*translateX\(8px\)[\s\S]*translateX\(0\)/); assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/); - assert.match(styles, /\.client-device-name \{[\s\S]*font: 700 15px\/1\.2/); - assert.match(styles, /\.client-device-last-seen \{[\s\S]*font-size: 10px/); + assert.match(styles, /\.client-device-name \{[\s\S]*font: 700 14px\/1\.2/); + assert.match(styles, /\.client-device-name-ip \{[\s\S]*font-size: 12px/); + assert.match(styles, /\.client-device-last-seen \{[\s\S]*position: absolute;[\s\S]*top: 0;[\s\S]*left: 0;[\s\S]*font-size: 8\.5px/); assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font-size: 10px/); assert.match(styles, /\.client-device-name > span \{[^}]*opacity: 0/); assert.match(styles, /\.client-device-name > \.client-device-name-primary \{[^}]*opacity: 1/); @@ -98,8 +101,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer', assert.match(styles, /\.client-device-name:hover \.client-device-name-ip[\s\S]*opacity: 1[\s\S]*translateY\(0\) scale\(1\)/); assert.match(styles, /\.client-device-name\.is-copied \.client-device-name-feedback[\s\S]*opacity: 1/); assert.match(styles, /\.client-device-name-feedback \{[^}]*font-size: 9px/); - assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: center/); - assert.match(styles, /\.client-device-last-seen \{[^}]*height: 32px[\s\S]*align-items: center/); + assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding-top: 11px/); + 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-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/); assert.match(styles, /\.client-text-morph-value \{[\s\S]*transition: opacity 360ms[\s\S]*filter 480ms/);