Enhance device traffic feedback and totals
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.13.4',
|
macClient: '0.13.5',
|
||||||
gatewayClient: '0.14.4',
|
gatewayClient: '0.14.5',
|
||||||
gatewayBackend: '0.14.0',
|
gatewayBackend: '0.14.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import {
|
|||||||
byteString,
|
byteString,
|
||||||
formatByteString,
|
formatByteString,
|
||||||
formatLastSeen,
|
formatLastSeen,
|
||||||
|
positiveByteDelta,
|
||||||
sortDevicesByTraffic,
|
sortDevicesByTraffic,
|
||||||
} from '../utils/format.js';
|
} from '../utils/format.js';
|
||||||
|
|
||||||
const AUTO_REFRESH_MS = 15_000;
|
const AUTO_REFRESH_MS = 15_000;
|
||||||
const DEVICE_MOVE_MS = 520;
|
const DEVICE_MOVE_MS = 520;
|
||||||
|
const COPY_FEEDBACK_MS = 5_000;
|
||||||
|
const TRAFFIC_DELTA_MS = 2_200;
|
||||||
|
|
||||||
function Tooltip({ children }) {
|
function Tooltip({ children }) {
|
||||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
||||||
@@ -24,6 +27,13 @@ function TextMorph({ from, to }) {
|
|||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TrafficValue({ value, delta }) {
|
||||||
|
return <strong className={`client-device-traffic-value${delta ? ' has-delta' : ''}`}>
|
||||||
|
<span className="is-total">{value}</span>
|
||||||
|
<span className="is-delta">{delta ? `+${delta}` : ''}</span>
|
||||||
|
</strong>;
|
||||||
|
}
|
||||||
|
|
||||||
export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
||||||
const [snapshot, setSnapshot] = useState(null);
|
const [snapshot, setSnapshot] = useState(null);
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
@@ -35,9 +45,12 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
const [refreshCycle, setRefreshCycle] = useState(0);
|
const [refreshCycle, setRefreshCycle] = useState(0);
|
||||||
const [sortDirection, setSortDirection] = useState('desc');
|
const [sortDirection, setSortDirection] = useState('desc');
|
||||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||||
|
const [trafficDeltas, setTrafficDeltas] = useState({});
|
||||||
const deviceNodes = useRef(new Map());
|
const deviceNodes = useRef(new Map());
|
||||||
const previousPositions = useRef(new Map());
|
const previousPositions = useRef(new Map());
|
||||||
|
const previousTraffic = useRef(new Map());
|
||||||
const copyTimer = useRef(null);
|
const copyTimer = useRef(null);
|
||||||
|
const trafficDeltaTimer = useRef(null);
|
||||||
const devices = useMemo(
|
const devices = useMemo(
|
||||||
() => sortDevicesByTraffic(snapshot?.devices, sortDirection),
|
() => sortDevicesByTraffic(snapshot?.devices, sortDirection),
|
||||||
[snapshot?.devices, sortDirection],
|
[snapshot?.devices, sortDirection],
|
||||||
@@ -72,7 +85,37 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [open, refreshCycle, refreshing, status]);
|
}, [open, refreshCycle, refreshing, status]);
|
||||||
|
|
||||||
useEffect(() => () => clearTimeout(copyTimer.current), []);
|
useEffect(() => () => {
|
||||||
|
clearTimeout(copyTimer.current);
|
||||||
|
clearTimeout(trafficDeltaTimer.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
previousTraffic.current.clear();
|
||||||
|
clearTimeout(trafficDeltaTimer.current);
|
||||||
|
setTrafficDeltas({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = new Map();
|
||||||
|
const deltas = {};
|
||||||
|
for (const device of snapshot?.devices || []) {
|
||||||
|
const gateway = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
||||||
|
const proxy = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||||
|
const previous = previousTraffic.current.get(device.id);
|
||||||
|
next.set(device.id, { gateway, proxy });
|
||||||
|
if (!previous) continue;
|
||||||
|
const gatewayDelta = positiveByteDelta(previous.gateway, gateway);
|
||||||
|
const proxyDelta = positiveByteDelta(previous.proxy, proxy);
|
||||||
|
if (gatewayDelta || proxyDelta) deltas[device.id] = { gateway: gatewayDelta, proxy: proxyDelta };
|
||||||
|
}
|
||||||
|
previousTraffic.current = next;
|
||||||
|
if (!Object.keys(deltas).length) return;
|
||||||
|
setTrafficDeltas(deltas);
|
||||||
|
clearTimeout(trafficDeltaTimer.current);
|
||||||
|
trafficDeltaTimer.current = setTimeout(() => setTrafficDeltas({}), TRAFFIC_DELTA_MS);
|
||||||
|
}, [snapshot?.devices, open]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -171,7 +214,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
} catch {
|
} catch {
|
||||||
setCopyFeedback({ id: device.id, failed: true });
|
setCopyFeedback({ id: device.id, failed: true });
|
||||||
}
|
}
|
||||||
copyTimer.current = setTimeout(() => setCopyFeedback(null), 800);
|
copyTimer.current = setTimeout(() => setCopyFeedback(null), COPY_FEEDBACK_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -266,7 +309,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
||||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
|
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'copied!' : ''}
|
||||||
</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) => {
|
||||||
@@ -279,6 +322,8 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
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 totalTraffic = formatByteString((gatewayTotal + proxyTotal).toString());
|
||||||
|
const trafficDelta = trafficDeltas[device.id] || {};
|
||||||
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';
|
||||||
@@ -350,7 +395,7 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
<span className="client-device-name-primary">{title}</span>
|
<span className="client-device-name-primary">{title}</span>
|
||||||
<span className="client-device-name-ip" aria-hidden="true">{device.ip}</span>
|
<span className="client-device-name-ip" aria-hidden="true">{device.ip}</span>
|
||||||
<span className="client-device-name-feedback" aria-hidden="true">
|
<span className="client-device-name-feedback" aria-hidden="true">
|
||||||
{copied && copyFeedback.failed ? 'Ошибка' : 'Скопировано'}
|
{copied && copyFeedback.failed ? 'Ошибка' : 'copied!'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</h3> : <h3>{title}</h3>}
|
</h3> : <h3>{title}</h3>}
|
||||||
@@ -384,8 +429,9 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
|
|||||||
|
|
||||||
<span className="client-device-traffic" role="group" aria-label={`Gateway ${gatewayTraffic}, Прокси ${proxyTraffic}`}>
|
<span className="client-device-traffic" role="group" aria-label={`Gateway ${gatewayTraffic}, Прокси ${proxyTraffic}`}>
|
||||||
<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><TrafficValue value={gatewayTraffic} delta={trafficDelta.gateway} /></span>
|
||||||
<span className="is-proxy"><b>Прокси</b><strong>{proxyTraffic}</strong></span>
|
<span className="is-proxy"><b>Прокси</b><TrafficValue value={proxyTraffic} delta={trafficDelta.proxy} /></span>
|
||||||
|
<span className="is-total"><b>Всего</b><strong>{totalTraffic}</strong></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
+82
-1
@@ -909,6 +909,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-main {
|
.client-device-main {
|
||||||
|
height: 34px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -916,8 +917,11 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-main > h3 {
|
.client-device-main > h3 {
|
||||||
|
height: 32px;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -927,9 +931,11 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name {
|
.client-device-name {
|
||||||
|
height: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -945,22 +951,32 @@ p {
|
|||||||
grid-area: 1 / 1;
|
grid-area: 1 / 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
filter: blur(8px);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
transform: translateY(0.18em) scale(0.96);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name > .client-device-name-primary {
|
.client-device-name > .client-device-name-primary {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
filter: blur(0);
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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(8px);
|
||||||
|
transform: translateY(-0.18em) scale(1.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name.is-copied .client-device-name-primary,
|
.client-device-name.is-copied .client-device-name-primary,
|
||||||
@@ -968,11 +984,21 @@ 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(8px);
|
||||||
|
transform: translateY(-0.18em) scale(1.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-name-feedback {
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-name.is-copied .client-device-name-feedback {
|
.client-device-name.is-copied .client-device-name-feedback {
|
||||||
@@ -1066,8 +1092,11 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-device-last-seen {
|
.client-device-last-seen {
|
||||||
|
height: 32px;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
max-width: 116px;
|
max-width: 116px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -1077,6 +1106,12 @@ p {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-device-last-seen time {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.client-device-last-seen.is-online {
|
.client-device-last-seen.is-online {
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1155,7 +1190,7 @@ p {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-sources span {
|
.client-device-traffic-sources > span {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 46px minmax(0, 1fr);
|
grid-template-columns: 46px minmax(0, 1fr);
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
@@ -1178,6 +1213,41 @@ p {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-value {
|
||||||
|
min-width: 0;
|
||||||
|
display: inline-grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-value > span {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
justify-self: end;
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(6px);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transform: translateY(0.18em) scale(0.96);
|
||||||
|
transform-origin: right center;
|
||||||
|
transition: opacity 260ms ease, filter 360ms cubic-bezier(0.16, 1, 0.3, 1), transform 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-value > .is-total,
|
||||||
|
.client-device-traffic-value.has-delta > .is-delta {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0);
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-value.has-delta > .is-total {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(6px);
|
||||||
|
transform: translateY(-0.18em) scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-value > .is-delta {
|
||||||
|
color: var(--client-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.client-device-traffic-sources .is-proxy {
|
.client-device-traffic-sources .is-proxy {
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
}
|
}
|
||||||
@@ -1186,6 +1256,15 @@ 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-sources > .is-total {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--client-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-device-traffic-sources > .is-total b {
|
||||||
|
color: var(--client-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.client-device-policy-wrap {
|
.client-device-policy-wrap {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -4243,6 +4322,8 @@ 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-traffic-value > span,
|
||||||
.client-device-edit,
|
.client-device-edit,
|
||||||
.client-device-edit svg,
|
.client-device-edit svg,
|
||||||
.client-device-edit-wrap,
|
.client-device-edit-wrap,
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ export function formatByteString(value) {
|
|||||||
return `${tenths / 10n},${tenths % 10n} ${units[unit]}`;
|
return `${tenths / 10n},${tenths % 10n} ${units[unit]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function positiveByteDelta(previous, current) {
|
||||||
|
const before = byteString(previous);
|
||||||
|
const after = byteString(current);
|
||||||
|
return after > before ? formatByteString((after - before).toString()) : '';
|
||||||
|
}
|
||||||
|
|
||||||
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 : [])
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import {
|
import {
|
||||||
formatByteString,
|
formatByteString,
|
||||||
formatLastSeen,
|
formatLastSeen,
|
||||||
|
positiveByteDelta,
|
||||||
sortDevicesByTraffic,
|
sortDevicesByTraffic,
|
||||||
} from '../../src/web/utils/format.js';
|
} from '../../src/web/utils/format.js';
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(panel, /<Tooltip>Изменить название<\/Tooltip>/);
|
assert.match(panel, /<Tooltip>Изменить название<\/Tooltip>/);
|
||||||
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]*'copied!'/);
|
||||||
|
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.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.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\} \/>/);
|
||||||
@@ -46,8 +48,11 @@ 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, /<b>Gateway<\/b><strong>\{gatewayTraffic\}<\/strong>/);
|
assert.match(panel, /<b>Gateway<\/b><TrafficValue value=\{gatewayTraffic\} delta=\{trafficDelta\.gateway\}/);
|
||||||
assert.match(panel, /className="is-proxy"><b>Прокси<\/b><strong>\{proxyTraffic\}<\/strong>/);
|
assert.match(panel, /className="is-proxy"><b>Прокси<\/b><TrafficValue value=\{proxyTraffic\} delta=\{trafficDelta\.proxy\}/);
|
||||||
|
assert.match(panel, /className="is-total"><b>Всего<\/b><strong>\{totalTraffic\}<\/strong>/);
|
||||||
|
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)/);
|
||||||
|
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
|
||||||
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
|
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/);
|
||||||
@@ -66,16 +71,21 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device \{[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*padding: 10px 8px/);
|
assert.match(styles, /\.client-device \{[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*padding: 10px 8px/);
|
||||||
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-sources \{[\s\S]*display: grid/);
|
assert.match(styles, /\.client-device-traffic-sources \{[\s\S]*display: grid/);
|
||||||
assert.match(styles, /\.client-device-traffic-sources span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
|
assert.match(styles, /\.client-device-traffic-sources > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
|
||||||
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-name \{[\s\S]*font: 700 15px\/1\.2/);
|
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-last-seen \{[\s\S]*font-size: 10px/);
|
||||||
assert.match(styles, /\.client-device-traffic-sources strong \{[\s\S]*font-size: 10px/);
|
assert.match(styles, /\.client-device-traffic-sources strong \{[\s\S]*font-size: 10px/);
|
||||||
assert.match(styles, /\.client-device-name > span \{[^}]*opacity: 0/);
|
assert.match(styles, /\.client-device-name > span \{[^}]*opacity: 0/);
|
||||||
assert.match(styles, /\.client-device-name > \.client-device-name-primary \{[^}]*opacity: 1/);
|
assert.match(styles, /\.client-device-name > \.client-device-name-primary \{[^}]*opacity: 1/);
|
||||||
assert.match(styles, /\.client-device-name:hover \.client-device-name-ip[\s\S]*opacity: 1/);
|
assert.match(styles, /\.client-device-name > span \{[^}]*transition: opacity 360ms[^}]*transform 480ms/);
|
||||||
|
assert.match(styles, /\.client-device-name:hover \.client-device-name-primary[\s\S]*translateY\(-0\.18em\)/);
|
||||||
|
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\.is-copied \.client-device-name-feedback[\s\S]*opacity: 1/);
|
||||||
assert.doesNotMatch(styles, /\.client-device-name > span \{[^}]*transition:/);
|
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-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
|
||||||
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
|
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
|
||||||
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/);
|
||||||
@@ -85,7 +95,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-text-morph-value/);
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-name > span[\s\S]*\.client-device-traffic-value > span[\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/);
|
||||||
});
|
});
|
||||||
@@ -107,6 +117,9 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
|
|||||||
assert.equal(formatByteString('9007199254740993'), '8,0 ПБ');
|
assert.equal(formatByteString('9007199254740993'), '8,0 ПБ');
|
||||||
assert.equal(formatByteString('1536'), '1,5 КБ');
|
assert.equal(formatByteString('1536'), '1,5 КБ');
|
||||||
assert.equal(formatByteString('invalid'), '0 Б');
|
assert.equal(formatByteString('invalid'), '0 Б');
|
||||||
|
assert.equal(positiveByteDelta('1048576', '3145728'), '2,0 МБ');
|
||||||
|
assert.equal(positiveByteDelta('3145728', '3145728'), '');
|
||||||
|
assert.equal(positiveByteDelta('3145728', '1048576'), '');
|
||||||
|
|
||||||
const devices = [
|
const devices = [
|
||||||
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },
|
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },
|
||||||
|
|||||||
Reference in New Issue
Block a user