Refine device traffic display and sorting
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.13.1',
|
macClient: '0.13.2',
|
||||||
gatewayClient: '0.14.1',
|
gatewayClient: '0.14.2',
|
||||||
gatewayBackend: '0.14.0',
|
gatewayBackend: '0.14.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -275,15 +275,10 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
const saving = savingId === device.id;
|
const saving = savingId === device.id;
|
||||||
const seen = formatLastSeen(device.lastSeenAt);
|
const seen = formatLastSeen(device.lastSeenAt);
|
||||||
const online = device.status === 'online';
|
const online = device.status === 'online';
|
||||||
const download = formatByteString(device.downloadBytes);
|
|
||||||
const upload = formatByteString(device.uploadBytes);
|
|
||||||
const proxyDownload = formatByteString(device.proxyDownloadBytes);
|
|
||||||
const proxyUpload = formatByteString(device.proxyUploadBytes);
|
|
||||||
const gatewayTotal = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
const gatewayTotal = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
||||||
const proxyTotal = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
const proxyTotal = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||||
const gatewayTraffic = formatByteString(gatewayTotal.toString());
|
const gatewayTraffic = formatByteString(gatewayTotal.toString());
|
||||||
const proxyTraffic = formatByteString(proxyTotal.toString());
|
const proxyTraffic = formatByteString(proxyTotal.toString());
|
||||||
const trafficLabel = `Gateway: получено ${download}, отдано ${upload}. Прокси: получено ${proxyDownload}, отдано ${proxyUpload}`;
|
|
||||||
const copied = copyFeedback?.id === device.id;
|
const copied = copyFeedback?.id === device.id;
|
||||||
const policyBusy = device.policyStatus === 'applying';
|
const policyBusy = device.policyStatus === 'applying';
|
||||||
const policyFailed = device.policyStatus === 'failed';
|
const policyFailed = device.policyStatus === 'failed';
|
||||||
@@ -344,7 +339,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
<button type="button" aria-label="Отменить изменение" disabled={saving} onClick={() => setEditingId('')}>×</button>
|
<button type="button" aria-label="Отменить изменение" disabled={saving} onClick={() => setEditingId('')}>×</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<div className="client-device-title">
|
<>
|
||||||
{device.ip ? <h3 className="client-device-name-heading">
|
{device.ip ? <h3 className="client-device-name-heading">
|
||||||
<button
|
<button
|
||||||
className={`client-device-name${copied ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
className={`client-device-name${copied ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||||
@@ -375,7 +370,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
</button>
|
</button>
|
||||||
<Tooltip>Изменить название</Tooltip>
|
<Tooltip>Изменить название</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
<span className={`client-device-last-seen${online ? ' is-online' : ''}`} tabIndex="0">
|
<span className={`client-device-last-seen${online ? ' is-online' : ''}`} tabIndex="0">
|
||||||
<time
|
<time
|
||||||
@@ -387,16 +382,11 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
<span className="client-device-traffic" role="group" aria-label={`Gateway ${gatewayTraffic}, Прокси ${proxyTraffic}`}>
|
||||||
className="client-device-traffic client-tooltip-anchor"
|
|
||||||
tabIndex="0"
|
|
||||||
aria-label={trafficLabel}
|
|
||||||
>
|
|
||||||
<span className="client-device-traffic-sources" aria-hidden="true">
|
<span className="client-device-traffic-sources" aria-hidden="true">
|
||||||
<span><b>Gateway</b><strong>{gatewayTraffic}</strong></span>
|
<span><b>Gateway</b><strong>{gatewayTraffic}</strong></span>
|
||||||
<span className="is-proxy"><b>Прокси</b><strong>{proxyTraffic}</strong></span>
|
<span className="is-proxy"><b>Прокси</b><strong>{proxyTraffic}</strong></span>
|
||||||
</span>
|
</span>
|
||||||
<Tooltip>{trafficLabel}</Tooltip>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="client-device-policy-wrap client-tooltip-anchor">
|
<span className="client-device-policy-wrap client-tooltip-anchor">
|
||||||
|
|||||||
+2
-43
@@ -903,20 +903,9 @@ p {
|
|||||||
gap: 7px;
|
gap: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-title {
|
.client-device-main > h3 {
|
||||||
flex: 1 1 auto;
|
flex: 0 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-title h3 {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-title h3 {
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -944,32 +933,23 @@ p {
|
|||||||
grid-area: 1 / 1;
|
grid-area: 1 / 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(0);
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transform: translateY(0);
|
|
||||||
transition: opacity 220ms ease, filter 320ms ease, transform 320ms cubic-bezier(0.16, 1, 0.3, 1), color 220ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name-ip,
|
.client-device-name-ip,
|
||||||
.client-device-name-feedback {
|
.client-device-name-feedback {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name:hover .client-device-name-primary,
|
.client-device-name:hover .client-device-name-primary,
|
||||||
.client-device-name:focus-visible .client-device-name-primary {
|
.client-device-name:focus-visible .client-device-name-primary {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name:hover .client-device-name-ip,
|
.client-device-name:hover .client-device-name-ip,
|
||||||
.client-device-name:focus-visible .client-device-name-ip {
|
.client-device-name:focus-visible .client-device-name-ip {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name.is-copied .client-device-name-primary,
|
.client-device-name.is-copied .client-device-name-primary,
|
||||||
@@ -977,14 +957,11 @@ p {
|
|||||||
.client-device-name.is-copy-error .client-device-name-primary,
|
.client-device-name.is-copy-error .client-device-name-primary,
|
||||||
.client-device-name.is-copy-error .client-device-name-ip {
|
.client-device-name.is-copy-error .client-device-name-ip {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
filter: blur(5px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name.is-copied .client-device-name-feedback,
|
.client-device-name.is-copied .client-device-name-feedback,
|
||||||
.client-device-name.is-copy-error .client-device-name-feedback {
|
.client-device-name.is-copy-error .client-device-name-feedback {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name.is-copied .client-device-name-feedback {
|
.client-device-name.is-copied .client-device-name-feedback {
|
||||||
@@ -1198,23 +1175,6 @@ p {
|
|||||||
color: color-mix(in oklch, var(--client-accent) 72%, var(--client-muted));
|
color: color-mix(in oklch, var(--client-accent) 72%, var(--client-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic:focus-visible {
|
|
||||||
border-radius: 3px;
|
|
||||||
outline: 2px solid var(--client-accent);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-traffic.client-tooltip-anchor > .client-tooltip {
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
transform: translate(0, 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-traffic.client-tooltip-anchor:hover > .client-tooltip,
|
|
||||||
.client-device-traffic.client-tooltip-anchor:focus-visible > .client-tooltip {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-device-policy-wrap {
|
.client-device-policy-wrap {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -4272,7 +4232,6 @@ p {
|
|||||||
.client-device-pin svg,
|
.client-device-pin svg,
|
||||||
.client-device-policy,
|
.client-device-policy,
|
||||||
.client-device-policy svg,
|
.client-device-policy svg,
|
||||||
.client-device-name > span,
|
|
||||||
.client-device-edit,
|
.client-device-edit,
|
||||||
.client-device-edit svg,
|
.client-device-edit svg,
|
||||||
.client-device-edit-wrap,
|
.client-device-edit-wrap,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function formatByteString(value) {
|
|||||||
export function sortDevicesByTraffic(devices, direction = 'desc') {
|
export function sortDevicesByTraffic(devices, direction = 'desc') {
|
||||||
const factor = direction === 'asc' ? 1 : -1;
|
const factor = direction === 'asc' ? 1 : -1;
|
||||||
return (Array.isArray(devices) ? devices : [])
|
return (Array.isArray(devices) ? devices : [])
|
||||||
.map((device, index) => ({ device, index }))
|
.map((device) => ({ device }))
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const pinned = Number(right.device.pinned === true) - Number(left.device.pinned === true);
|
const pinned = Number(right.device.pinned === true) - Number(left.device.pinned === true);
|
||||||
if (pinned) return pinned;
|
if (pinned) return pinned;
|
||||||
@@ -42,7 +42,9 @@ export function sortDevicesByTraffic(devices, direction = 'desc') {
|
|||||||
+ byteString(left.device.proxyUploadBytes) + byteString(left.device.proxyDownloadBytes);
|
+ byteString(left.device.proxyUploadBytes) + byteString(left.device.proxyDownloadBytes);
|
||||||
const rightTotal = byteString(right.device.uploadBytes) + byteString(right.device.downloadBytes)
|
const rightTotal = byteString(right.device.uploadBytes) + byteString(right.device.downloadBytes)
|
||||||
+ byteString(right.device.proxyUploadBytes) + byteString(right.device.proxyDownloadBytes);
|
+ byteString(right.device.proxyUploadBytes) + byteString(right.device.proxyDownloadBytes);
|
||||||
if (leftTotal === rightTotal) return left.index - right.index;
|
if (leftTotal === rightTotal) {
|
||||||
|
return String(left.device.id || '').localeCompare(String(right.device.id || ''));
|
||||||
|
}
|
||||||
return (leftTotal < rightTotal ? -1 : 1) * factor;
|
return (leftTotal < rightTotal ? -1 : 1) * factor;
|
||||||
})
|
})
|
||||||
.map(({ device }) => device);
|
.map(({ device }) => device);
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /copyText\(device\.ip\)/);
|
assert.match(panel, /copyText\(device\.ip\)/);
|
||||||
assert.match(panel, /client-device-name-primary[\s\S]*client-device-name-ip[\s\S]*client-device-name-feedback/);
|
assert.match(panel, /client-device-name-primary[\s\S]*client-device-name-ip[\s\S]*client-device-name-feedback/);
|
||||||
assert.match(panel, /client-device-name-feedback[\s\S]*'Скопировано'/);
|
assert.match(panel, /client-device-name-feedback[\s\S]*'Скопировано'/);
|
||||||
|
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 \? 'В сети' : <TextMorph from=\{seen\.label\} to=\{seen\.relative\} \/>/);
|
assert.match(panel, /online \? 'В сети' : <TextMorph from=\{seen\.label\} to=\{seen\.relative\} \/>/);
|
||||||
assert.match(panel, /aria-label=\{`\$\{online \? 'В сети' : 'Не в сети'\}\. Последний контакт: \$\{seen\.tooltip\}`\}/);
|
assert.match(panel, /aria-label=\{`\$\{online \? 'В сети' : 'Не в сети'\}\. Последний контакт: \$\{seen\.tooltip\}`\}/);
|
||||||
assert.doesNotMatch(panel, /client-device-status|STATUS_LABELS/);
|
assert.doesNotMatch(panel, /client-device-status|STATUS_LABELS/);
|
||||||
@@ -44,10 +46,9 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /sortDevicesByTraffic\(snapshot\?\.devices, sortDirection\)/);
|
assert.match(panel, /sortDevicesByTraffic\(snapshot\?\.devices, sortDirection\)/);
|
||||||
assert.match(panel, /Трафик временно не обновляется/);
|
assert.match(panel, /Трафик временно не обновляется/);
|
||||||
assert.match(panel, /Учитывается только трафик, который прошёл через Harbor/);
|
assert.match(panel, /Учитывается только трафик, который прошёл через Harbor/);
|
||||||
assert.match(panel, /Gateway: получено \$\{download\}, отдано \$\{upload\}/);
|
|
||||||
assert.match(panel, /Прокси: получено \$\{proxyDownload\}, отдано \$\{proxyUpload\}/);
|
|
||||||
assert.match(panel, /<b>Gateway<\/b><strong>\{gatewayTraffic\}<\/strong>/);
|
assert.match(panel, /<b>Gateway<\/b><strong>\{gatewayTraffic\}<\/strong>/);
|
||||||
assert.match(panel, /className="is-proxy"><b>Прокси<\/b><strong>\{proxyTraffic\}<\/strong>/);
|
assert.match(panel, /className="is-proxy"><b>Прокси<\/b><strong>\{proxyTraffic\}<\/strong>/);
|
||||||
|
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
|
||||||
assert.match(panel, /source\?\.traffic\?\.proxy\?\.error/);
|
assert.match(panel, /source\?\.traffic\?\.proxy\?\.error/);
|
||||||
assert.match(panel, /client-device-pin-wrap[\s\S]*client-device-main[\s\S]*client-device-traffic[\s\S]*client-device-policy-wrap/);
|
assert.match(panel, /client-device-pin-wrap[\s\S]*client-device-main[\s\S]*client-device-traffic[\s\S]*client-device-policy-wrap/);
|
||||||
assert.doesNotMatch(panel, /client-device-details/);
|
assert.doesNotMatch(panel, /client-device-details/);
|
||||||
@@ -69,6 +70,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 32px;[\s\S]*border-radius: 50%/);
|
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 32px;[\s\S]*border-radius: 50%/);
|
||||||
assert.match(styles, /\.client-device-name:hover \.client-device-name-ip[\s\S]*opacity: 1/);
|
assert.match(styles, /\.client-device-name:hover \.client-device-name-ip[\s\S]*opacity: 1/);
|
||||||
assert.match(styles, /\.client-device-name\.is-copied \.client-device-name-feedback[\s\S]*opacity: 1/);
|
assert.match(styles, /\.client-device-name\.is-copied \.client-device-name-feedback[\s\S]*opacity: 1/);
|
||||||
|
assert.doesNotMatch(styles, /\.client-device-name > span \{[^}]*transition:/);
|
||||||
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\)/);
|
||||||
assert.match(styles, /\.client-text-morph-value \{[\s\S]*transition: opacity 360ms[\s\S]*filter 480ms/);
|
assert.match(styles, /\.client-text-morph-value \{[\s\S]*transition: opacity 360ms[\s\S]*filter 480ms/);
|
||||||
assert.match(styles, /\.client-device-last-seen:hover \.client-text-morph-value\.is-relative[\s\S]*opacity: 1/);
|
assert.match(styles, /\.client-device-last-seen:hover \.client-text-morph-value\.is-relative[\s\S]*opacity: 1/);
|
||||||
@@ -78,7 +80,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-edit:hover svg[\s\S]*translate\(1px, -1px\) rotate\(-4deg\)/);
|
assert.match(styles, /\.client-device-edit:hover svg[\s\S]*translate\(1px, -1px\) rotate\(-4deg\)/);
|
||||||
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-name > span[\s\S]*\.client-text-morph-value/);
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-text-morph-value/);
|
||||||
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/);
|
||||||
});
|
});
|
||||||
@@ -104,8 +106,8 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
|
|||||||
const devices = [
|
const devices = [
|
||||||
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },
|
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },
|
||||||
{ id: 'b', uploadBytes: '9007199254740992', downloadBytes: '2' },
|
{ id: 'b', uploadBytes: '9007199254740992', downloadBytes: '2' },
|
||||||
{ id: 'c', uploadBytes: '10', downloadBytes: '10', proxyDownloadBytes: '100' },
|
|
||||||
{ id: 'd', uploadBytes: '15', downloadBytes: '5', proxyUploadBytes: '100' },
|
{ id: 'd', uploadBytes: '15', downloadBytes: '5', proxyUploadBytes: '100' },
|
||||||
|
{ id: 'c', uploadBytes: '10', downloadBytes: '10', proxyDownloadBytes: '100' },
|
||||||
{ id: 'e', pinned: true, uploadBytes: '0', downloadBytes: '0' },
|
{ id: 'e', pinned: true, uploadBytes: '0', downloadBytes: '0' },
|
||||||
];
|
];
|
||||||
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd']);
|
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd']);
|
||||||
|
|||||||
Reference in New Issue
Block a user