Improve gateway traffic dashboard and connection branding
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Сколько секунд прошло с последнего успешного обновления. До 60 секунд — штатно, 60–119 — задержка, от 120 — данные устарели.",
|
||||
"description": "Сколько секунд прошло с последнего успешного обновления к концу выбранного периода. До 60 секунд — штатно, 60–119 — задержка, от 120 — данные устарели.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -106,7 +106,7 @@
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Свежесть данных сейчас",
|
||||
"title": "Свежесть данных в конце выбранного периода",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
@@ -114,18 +114,24 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Текущая суммарная скорость всех устройств, сглаженная за последние 5 минут.",
|
||||
"description": "Суммарная скорость всех устройств в выбранном периоде. Окно усреднения автоматически адаптируется к масштабу времени; скачивание показано вверх, отправка — вниз.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": true,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 4,
|
||||
"scaleDistribution": {
|
||||
"linearThreshold": 1048576,
|
||||
"log": 2,
|
||||
"type": "symlog"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
@@ -167,6 +173,18 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byFrameRefID",
|
||||
"options": "B"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.transform",
|
||||
"value": "negative-Y"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -195,20 +213,20 @@
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"download\"}[5m])) > 0",
|
||||
"expr": "sum(rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"download\"}[$__rate_interval])) > 0",
|
||||
"legendFormat": "Скачивание",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"upload\"}[5m])) > 0",
|
||||
"expr": "sum(rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"upload\"}[$__rate_interval])) > 0",
|
||||
"legendFormat": "Отправка",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Скорость сейчас, средняя за 5 минут",
|
||||
"title": "Скорость за выбранный период",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
@@ -345,13 +363,14 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Суммарная скорость Gateway/Proxy и upload/download для каждого активного устройства из фильтра, средняя за последние 5 минут. Нулевые устройства скрыты.",
|
||||
"description": "Скорость скачивания и отправки каждого активного устройства в выбранном периоде. Окно усреднения адаптируется к масштабу времени; скачивание показано вверх, отправка — вниз. Нулевые series скрыты.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": true,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 6,
|
||||
"lineInterpolation": "smooth",
|
||||
@@ -373,7 +392,20 @@
|
||||
"noValue": "Нет активного трафика",
|
||||
"unit": "Bps"
|
||||
},
|
||||
"overrides": []
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byFrameRefID",
|
||||
"options": "B"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.transform",
|
||||
"value": "negative-Y"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
@@ -401,13 +433,20 @@
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "(sum by (device_id) (rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\"}[5m])) > 0) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info)",
|
||||
"legendFormat": "{{name}} · {{ip}}",
|
||||
"expr": "(sum by (device_id) (rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"download\"}[$__rate_interval])) > 0) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info)",
|
||||
"legendFormat": "{{name}} · {{ip}} · скачивание",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "(sum by (device_id) (rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"upload\"}[$__rate_interval])) > 0) * on (device_id) group_left (name, ip) max by (device_id, name, ip) (harbor_device_info)",
|
||||
"legendFormat": "{{name}} · {{ip}} · отправка",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Скорость по устройствам сейчас",
|
||||
"title": "Скорость по устройствам",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
@@ -415,18 +454,24 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Скорость выбранного scope по Gateway/Proxy и upload/download, средняя за последние 5 минут. При выборе «Все устройства» остаётся максимум четыре суммарные series; нулевые series скрыты.",
|
||||
"description": "Скорость выбранного scope по Gateway/Proxy и направлениям в выбранном периоде. Окно усреднения адаптируется к масштабу времени; скачивание показано вверх, отправка — вниз. Нулевые series скрыты.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": true,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 8,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineWidth": 2,
|
||||
"pointSize": 4,
|
||||
"scaleDistribution": {
|
||||
"linearThreshold": 1048576,
|
||||
"log": 2,
|
||||
"type": "symlog"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
@@ -438,7 +483,20 @@
|
||||
"noValue": "Нет данных за период",
|
||||
"unit": "Bps"
|
||||
},
|
||||
"overrides": []
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byFrameRefID",
|
||||
"options": "B"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.transform",
|
||||
"value": "negative-Y"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
@@ -466,13 +524,20 @@
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (source, direction) (rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\"}[5m])) > 0",
|
||||
"legendFormat": "{{source}} · {{direction}}",
|
||||
"expr": "sum by (source) (rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"download\"}[$__rate_interval])) > 0",
|
||||
"legendFormat": "{{source}} · скачивание",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (source) (rate(harbor_device_traffic_bytes_total{device_id=~\"$device_id\", direction=\"upload\"}[$__rate_interval])) > 0",
|
||||
"legendFormat": "{{source}} · отправка",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Скорость по источникам и направлениям сейчас",
|
||||
"title": "Скорость по источникам и направлениям",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
@@ -480,7 +545,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Накопленные счётчики выбранного scope за всё время хранения Harbor. При «Все устройства» счётчики суммируются; нулевые series скрыты.",
|
||||
"description": "Трафик выбранного scope в текущем диапазоне времени. При «Все устройства» значения суммируются; нулевые series скрыты.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -520,14 +585,14 @@
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (source, direction) (harbor_device_traffic_bytes_total{device_id=~\"$device_id\"}) > 0",
|
||||
"expr": "sum by (source, direction) (increase(harbor_device_traffic_bytes_total{device_id=~\"$device_id\"}[$__range])) > 0",
|
||||
"instant": true,
|
||||
"legendFormat": "{{source}} · {{direction}}",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Накоплено по источникам и направлениям",
|
||||
"title": "Трафик по источникам и направлениям за выбранный период",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
@@ -700,7 +765,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Накопленный объём всех счётчиков Harbor за всё время хранения.",
|
||||
"description": "Объём всех счётчиков Harbor в выбранном диапазоне времени.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -737,14 +802,14 @@
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum(harbor_traffic_bytes_total)",
|
||||
"expr": "sum(increase(harbor_traffic_bytes_total[$__range]))",
|
||||
"instant": true,
|
||||
"legendFormat": "Учтено Harbor",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Накоплено: весь Harbor",
|
||||
"title": "Весь Harbor за выбранный период",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
@@ -752,7 +817,7 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "Накопленные счётчики за всё время хранения отдельно для Gateway и Proxy.",
|
||||
"description": "Объём трафика Gateway и Proxy в выбранном диапазоне времени.",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
@@ -823,14 +888,14 @@
|
||||
"targets": [
|
||||
{
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (source) (harbor_traffic_bytes_total) > 0",
|
||||
"expr": "sum by (source) (increase(harbor_traffic_bytes_total[$__range])) > 0",
|
||||
"instant": true,
|
||||
"legendFormat": "{{source}}",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Накоплено: источники",
|
||||
"title": "Источники за выбранный период",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
@@ -866,7 +931,7 @@
|
||||
"query": "query_result(label_join(max by (device_id, name, ip) (harbor_device_info), \"display\", \" · \", \"name\", \"ip\"))",
|
||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"refresh": 2,
|
||||
"regex": "/display=\"(?<text>[^\"]+)\"|device_id=\"(?<value>[^\"]+)\"/g",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
@@ -891,6 +956,6 @@
|
||||
"timezone": "browser",
|
||||
"title": "Harbor Gateway: трафик",
|
||||
"uid": "harbor-gateway-traffic",
|
||||
"version": 6,
|
||||
"version": 7,
|
||||
"weekStart": "monday"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const HARBOR_VERSIONS = Object.freeze({
|
||||
macClient: '0.21.5',
|
||||
gatewayClient: '0.22.0',
|
||||
macClient: '0.21.6',
|
||||
gatewayClient: '0.22.1',
|
||||
gatewayBackend: '0.22.5',
|
||||
});
|
||||
|
||||
|
||||
@@ -537,14 +537,6 @@ export function ClientOverviewPage({
|
||||
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
||||
<span key={copyAnnouncement.cycle}>{copyAnnouncement.text}</span>
|
||||
</div>
|
||||
<HarborBrand
|
||||
isGateway={isGateway}
|
||||
connected={connected}
|
||||
gatewayAvailable={gatewayAvailable}
|
||||
gatewayDirect={gatewayDirect}
|
||||
blocked={gatewayAutoBlocked}
|
||||
onSetGatewayAuto={onSetGatewayAuto}
|
||||
/>
|
||||
{(isGateway || (hasSubscription && subscriptionContentReady)) && <nav className="client-secondary-menu" aria-label="Дополнительные меню">
|
||||
{isGateway && <SubscriptionToggle
|
||||
feature={subscriptionFeature}
|
||||
@@ -609,6 +601,14 @@ export function ClientOverviewPage({
|
||||
proxyPort={state?.clientRuntime?.proxyPort}
|
||||
now={now}
|
||||
blocked={connectionBlocked}
|
||||
brandSlot={<HarborBrand
|
||||
isGateway={isGateway}
|
||||
connected={connected}
|
||||
gatewayAvailable={gatewayAvailable}
|
||||
gatewayDirect={gatewayDirect}
|
||||
blocked={gatewayAutoBlocked}
|
||||
onSetGatewayAuto={onSetGatewayAuto}
|
||||
/>}
|
||||
copyFeedback={copyFeedback}
|
||||
onCopyProxy={copyProxy}
|
||||
onApply={onApply}
|
||||
|
||||
@@ -34,6 +34,7 @@ interface ConnectionPanelProps {
|
||||
proxyPort?: number;
|
||||
now: number;
|
||||
blocked: boolean;
|
||||
brandSlot?: ReactNode;
|
||||
copyFeedback?: Partial<Record<CopyKind, CopyFeedback>>;
|
||||
routingSlot?: ReactNode;
|
||||
serverSlot?: ReactNode;
|
||||
@@ -74,6 +75,7 @@ export function ConnectionPanel({
|
||||
proxyPort,
|
||||
now,
|
||||
blocked,
|
||||
brandSlot,
|
||||
copyFeedback,
|
||||
routingSlot,
|
||||
serverSlot,
|
||||
@@ -161,18 +163,19 @@ export function ConnectionPanel({
|
||||
</button>;
|
||||
|
||||
return <>
|
||||
{visible && <section className={`client-power-section${isGateway ? ' is-gateway' : ''}`} aria-labelledby="connection-title">
|
||||
{isGateway ? <span
|
||||
className="client-power-control client-tooltip-anchor"
|
||||
{visible ? <section className={`client-power-section${isGateway ? ' is-gateway' : ''}`} aria-labelledby="connection-title">
|
||||
<div
|
||||
className={`client-power-control${isGateway ? ' client-tooltip-anchor' : ''}`}
|
||||
tabIndex={powerUnavailable ? 0 : undefined}
|
||||
aria-label={powerUnavailable ? 'VPN недоступен' : undefined}
|
||||
aria-describedby={powerUnavailable ? 'gateway-power-unavailable' : undefined}
|
||||
>
|
||||
{brandSlot}
|
||||
{powerButton}
|
||||
{powerUnavailable && <span className="client-tooltip" id="gateway-power-unavailable" role="tooltip">
|
||||
Сначала добавьте подписку и выберите сервер
|
||||
</span>}
|
||||
</span> : powerButton}
|
||||
</div>
|
||||
<div className="client-state-detail">
|
||||
{connected ? (
|
||||
<button
|
||||
@@ -266,7 +269,7 @@ export function ConnectionPanel({
|
||||
</div>
|
||||
</section>
|
||||
{statusSlot}
|
||||
</section>}
|
||||
</section> : brandSlot}
|
||||
|
||||
<ConfirmationDialog
|
||||
open={confirmingStop}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
}
|
||||
|
||||
.client-power-control {
|
||||
position: relative;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
display: grid;
|
||||
@@ -38,18 +39,31 @@
|
||||
|
||||
.client-power-section.is-gateway .client-power-control,
|
||||
.client-power-section.is-gateway .client-power {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.client-power-section.is-gateway .client-power svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.client-power-section.is-gateway .client-power::before {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.client-power-control > .client-tooltip {
|
||||
top: calc(100% + 12px);
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -2px);
|
||||
}
|
||||
|
||||
.client-power-control.client-tooltip-anchor:hover > .client-tooltip,
|
||||
.client-power-control.client-tooltip-anchor:focus-visible > .client-tooltip {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.client-power-control:focus-visible {
|
||||
|
||||
+19
-11
@@ -152,6 +152,17 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.client-power-control > .harbor-brand {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: calc(100% + 44px);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
transform: translateX(-50%) scale(3);
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.harbor-brand.is-switchable {
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -206,13 +217,6 @@
|
||||
filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 44%, transparent));
|
||||
}
|
||||
|
||||
.harbor-brand.is-connected .harbor-brand-content > svg,
|
||||
.harbor-brand.is-connected .harbor-brand-name > strong,
|
||||
.harbor-brand.is-connected .harbor-brand-name > em,
|
||||
.harbor-brand.is-connected .harbor-mode-stack {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.harbor-brand.is-connected .harbor-brand-name > strong,
|
||||
.harbor-brand.is-connected .harbor-brand-name > em,
|
||||
.harbor-brand.is-connected .harbor-mode-stack {
|
||||
@@ -397,10 +401,6 @@
|
||||
animation: harbor-mode-arrows-discovered 260ms 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.harbor-brand.is-connected .harbor-mode-swap {
|
||||
transform: translateY(calc(-50% - 4px));
|
||||
}
|
||||
|
||||
@keyframes harbor-mode-arrows-discovered {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -662,6 +662,10 @@
|
||||
transform: translateX(-50%) scale(1.55);
|
||||
}
|
||||
|
||||
.client-power-control > .harbor-brand {
|
||||
transform: translateX(-50%) scale(1.55);
|
||||
}
|
||||
|
||||
.client-inline-error.is-connection,
|
||||
.client-inline-error.is-subscription {
|
||||
position: static;
|
||||
@@ -719,6 +723,10 @@
|
||||
transform: translateX(-50%) scale(1.35);
|
||||
}
|
||||
|
||||
.client-power-control > .harbor-brand {
|
||||
transform: translateX(-50%) scale(1.35);
|
||||
}
|
||||
|
||||
.client-secondary-menu {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@ test('connection feature is the sole always-mounted power panel owner', () => {
|
||||
assert.equal((page.match(/<ConnectionPanel/g) || []).length, 1);
|
||||
assert.match(page, /<main[\s\S]*<ConnectionPanel[\s\S]*<GatewayTrafficSummary/);
|
||||
assert.doesNotMatch(page, /client-power-section|const powerButton|function toggleConnection|confirmingStop|id="stop-connection"|DURATION_MODE_STORAGE_KEY/);
|
||||
assert.match(panel, /\{visible && <section className=\{`client-power-section\$\{isGateway \? ' is-gateway' : ''\}`\}/);
|
||||
assert.match(panel, /\{visible \? <section className=\{`client-power-section\$\{isGateway \? ' is-gateway' : ''\}`\}/);
|
||||
assert.match(panel, /<ConfirmationDialog[\s\S]*open=\{confirmingStop\}/);
|
||||
assert.match(panel, /<div[\s\S]*client-power-control[\s\S]*\{brandSlot\}[\s\S]*\{powerButton\}[\s\S]*<\/div>/);
|
||||
assert.match(page, /brandSlot=\{<HarborBrand[\s\S]*onSetGatewayAuto=\{onSetGatewayAuto\}[\s\S]*\/>\}/);
|
||||
assert.match(panel, /<\/section> : brandSlot/);
|
||||
assert.ok(panel.indexOf('<ConfirmationDialog') > panel.indexOf('{visible && <section'), 'dialog remains mounted outside the visible section');
|
||||
});
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
|
||||
assert.match(subscription, /const confirmingDeleteRef = useRef\(confirmingDelete\)[\s\S]*if \(confirmingDeleteRef\.current\) return/);
|
||||
assert.match(subscription, /requestDelete: \(\) => setConfirmingDelete\(true\)[\s\S]*open=\{feature\.confirmingDelete\}[\s\S]*onCancel=\{feature\.cancelDelete\}/);
|
||||
assert.match(connection, /aria-label=\{isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||||
assert.match(connection, /className="client-power-control client-tooltip-anchor"[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||||
assert.match(connection, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*\{isGateway \? <span[\s\S]*<\/span> : powerButton\}/);
|
||||
assert.match(connection, /className=\{`client-power-control\$\{isGateway \? ' client-tooltip-anchor' : ''\}`\}[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||||
assert.match(connection, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*client-power-control[\s\S]*\{brandSlot\}[\s\S]*\{powerButton\}/);
|
||||
assert.match(overview, /isGateway && <DevicesPanel[\s\S]*feature=\{devicesFeature\}/);
|
||||
assert.doesNotMatch(panel, /const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
|
||||
});
|
||||
|
||||
@@ -38,24 +38,31 @@ test('Grafana dashboard uses one all-or-one device scope and shows active device
|
||||
const topDevices = dashboard.panels.find(({ id }) => id === 5);
|
||||
const sourceSpeed = dashboard.panels.find(({ id }) => id === 6);
|
||||
const deviceSpeed = dashboard.panels.find(({ id }) => id === 14);
|
||||
const sourceTraffic = dashboard.panels.find(({ id }) => id === 7);
|
||||
const services = dashboard.panels.find(({ id }) => id === 8);
|
||||
const domains = dashboard.panels.find(({ id }) => id === 9);
|
||||
const totalTraffic = dashboard.panels.find(({ id }) => id === 1);
|
||||
const trafficBySource = dashboard.panels.find(({ id }) => id === 4);
|
||||
const speedPanels = [globalSpeed, deviceSpeed, sourceSpeed];
|
||||
|
||||
assert.equal(dashboard.title, 'Harbor Gateway: трафик');
|
||||
assert.equal(dashboard.refresh, '30s');
|
||||
assert.equal(dashboard.version, 7);
|
||||
assert.deepEqual(titles.filter((title) => ['Обзор', 'Устройства', 'Сервисы и домены', 'Техническая детализация'].includes(title)), [
|
||||
'Обзор',
|
||||
'Устройства',
|
||||
'Сервисы и домены',
|
||||
'Техническая детализация',
|
||||
]);
|
||||
assert.ok(titles.includes('Скорость сейчас, средняя за 5 минут'));
|
||||
assert.ok(titles.includes('Скорость по устройствам сейчас'));
|
||||
assert.ok(titles.includes('Скорость по источникам и направлениям сейчас'));
|
||||
assert.ok(titles.includes('Скорость за выбранный период'));
|
||||
assert.ok(titles.includes('Скорость по устройствам'));
|
||||
assert.ok(titles.includes('Скорость по источникам и направлениям'));
|
||||
assert.ok(titles.includes('Топ-10 устройств за выбранный период'));
|
||||
assert.ok(titles.includes('Накоплено по источникам и направлениям'));
|
||||
assert.ok(titles.includes('Трафик по источникам и направлениям за выбранный период'));
|
||||
assert.ok(titles.includes('Сервисы за выбранный период'));
|
||||
assert.ok(titles.includes('Домены за выбранный период'));
|
||||
assert.ok(titles.includes('Весь Harbor за выбранный период'));
|
||||
assert.ok(titles.includes('Источники за выбранный период'));
|
||||
assert.ok(expressions.some((expression) => expression.includes('harbor_traffic_bytes_total')));
|
||||
assert.ok(expressions.some((expression) => expression.includes('harbor_device_traffic_bytes_total')));
|
||||
assert.ok(expressions.some((expression) => expression.includes('harbor_device_domain_traffic_bytes_total{device_id=~"$device_id"}[$__range]')));
|
||||
@@ -66,24 +73,40 @@ test('Grafana dashboard uses one all-or-one device scope and shows active device
|
||||
assert.doesNotMatch(topDevices.fieldConfig.overrides[1].properties[0].value[0].url, /domain_device_id/);
|
||||
assert.deepEqual(freshness.fieldConfig.defaults.thresholds.steps.map(({ value }) => value), [null, 60, 120]);
|
||||
assert.equal(freshness.fieldConfig.defaults.noValue, 'Нет данных');
|
||||
assert.equal(freshness.title, 'Свежесть данных в конце выбранного периода');
|
||||
assert.equal(deviceVariable.name, 'device_id');
|
||||
assert.equal(deviceVariable.multi, false);
|
||||
assert.equal(deviceVariable.includeAll, true);
|
||||
assert.equal(deviceVariable.allValue, '.*');
|
||||
assert.equal(deviceVariable.current.value, '$__all');
|
||||
assert.equal(deviceVariable.refresh, 2);
|
||||
assert.equal(dashboard.templating.list.length, 1);
|
||||
assert.match(deviceVariable.query.query, /query_result\(label_join\(/);
|
||||
assert.match(deviceVariable.regex, /<text>/);
|
||||
assert.match(deviceVariable.regex, /<value>/);
|
||||
assert.ok(globalSpeed.targets.every(({ expr }) => expr.includes('device_id=~"$device_id"') && expr.endsWith('> 0')));
|
||||
assert.match(deviceSpeed.targets[0].expr, /sum by \(device_id\)[\s\S]*device_id=~"\$device_id"[\s\S]*> 0[\s\S]*group_left \(name, ip\)/);
|
||||
assert.match(deviceSpeed.targets[0].legendFormat, /\{\{name\}\} · \{\{ip\}\}/);
|
||||
assert.deepEqual(deviceSpeed.fieldConfig.defaults.custom.scaleDistribution, {
|
||||
for (const panel of speedPanels) {
|
||||
assert.equal(panel.fieldConfig.defaults.custom.axisCenteredZero, true);
|
||||
assert.deepEqual(panel.fieldConfig.defaults.custom.scaleDistribution, {
|
||||
linearThreshold: 1048576,
|
||||
log: 2,
|
||||
type: 'symlog',
|
||||
});
|
||||
assert.match(sourceSpeed.targets[0].expr, /device_id=~"\$device_id"[\s\S]*> 0$/);
|
||||
assert.deepEqual(panel.targets.map(({ refId }) => refId), ['A', 'B']);
|
||||
assert.ok(panel.targets.every(({ expr }) => expr.includes('[$__rate_interval]') && !expr.includes('[5m]')));
|
||||
assert.ok(panel.fieldConfig.overrides.some((override) => (
|
||||
override.matcher.id === 'byFrameRefID'
|
||||
&& override.matcher.options === 'B'
|
||||
&& override.properties.some(({ id, value }) => id === 'custom.transform' && value === 'negative-Y')
|
||||
)));
|
||||
}
|
||||
assert.ok(globalSpeed.targets.every(({ expr }) => expr.includes('device_id=~"$device_id"') && expr.endsWith('> 0')));
|
||||
assert.ok(deviceSpeed.targets.every(({ expr }) => /sum by \(device_id\)[\s\S]*device_id=~"\$device_id"[\s\S]*> 0[\s\S]*group_left \(name, ip\)/.test(expr)));
|
||||
assert.ok(deviceSpeed.targets.every(({ legendFormat }) => /\{\{name\}\} · \{\{ip\}\}/.test(legendFormat)));
|
||||
assert.ok(sourceSpeed.targets.every(({ expr }) => /device_id=~"\$device_id"[\s\S]*> 0$/.test(expr)));
|
||||
assert.equal(totalTraffic.targets[0].expr, 'sum(increase(harbor_traffic_bytes_total[$__range]))');
|
||||
assert.equal(trafficBySource.targets[0].expr, 'sum by (source) (increase(harbor_traffic_bytes_total[$__range])) > 0');
|
||||
assert.match(sourceTraffic.targets[0].expr, /increase\(harbor_device_traffic_bytes_total\{device_id=~"\$device_id"\}\[\$__range\]\)/);
|
||||
assert.ok(dashboard.panels.every(({ title = '', description = '' }) => !/Накоплено|за всё время/.test(`${title} ${description}`)));
|
||||
assert.match(services.targets[0].expr, /device_id=~"\$device_id"[\s\S]*> 0\)$/);
|
||||
assert.deepEqual(domains.transformations[0].options.indexByName, { service: 0, domain: 1, Value: 2 });
|
||||
assert.match(domains.targets[0].expr, /^topk\(15,[\s\S]*device_id=~"\$device_id"[\s\S]*> 0\)$/);
|
||||
|
||||
@@ -41,7 +41,11 @@ test('desktop layout keeps the power control on a symmetric center axis', () =>
|
||||
assert.match(styles, /\.client-panel\.has-subscription \.client-form-content \{[\s\S]*padding-top:\s*var\(--client-power-top\)/);
|
||||
assert.match(rule('.client-panel.is-gateway-home'), /min-height:\s*max\(620px, calc\(100dvh - 80px\)\)[\s\S]*grid-template-rows:\s*minmax\(260px, 1fr\) auto[\s\S]*row-gap:\s*36px/);
|
||||
assert.match(rule('.client-gateway-summary'), /grid-column:\s*1 \/ -1[\s\S]*grid-row:\s*2[\s\S]*justify-self:\s*center[\s\S]*width:\s*min\(860px, calc\(100% - 48px\)\)/);
|
||||
assert.match(styles, /\.client-power-section\.is-gateway \.client-power-control,[\s\S]*width:\s*68px;[\s\S]*height:\s*68px/);
|
||||
assert.match(styles, /\.client-power-section\.is-gateway \.client-power-control,[\s\S]*width:\s*84px;[\s\S]*height:\s*84px/);
|
||||
assert.match(rule('.client-power-control'), /position:\s*relative/);
|
||||
assert.match(rule('.client-power-control > .harbor-brand'), /position:\s*absolute[\s\S]*bottom:\s*calc\(100% \+ 44px\)[\s\S]*left:\s*50%[\s\S]*transform:\s*translateX\(-50%\) scale\(3\)[\s\S]*transform-origin:\s*center bottom/);
|
||||
assert.match(rule('.client-power-control > .client-tooltip'), /top:\s*calc\(100% \+ 12px\)[\s\S]*left:\s*50%[\s\S]*translate\(-50%, -2px\)/);
|
||||
assert.doesNotMatch(styles, /\.harbor-brand\.is-connected[^}]*transform|translateY\(calc\(-50% - 4px\)\)/);
|
||||
assert.match(rule('.client-gateway-traffic-chart'), /min-height:\s*180px/);
|
||||
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-gateway-traffic-chart,[\s\S]*min-height:\s*130px;[\s\S]*height:\s*130px/);
|
||||
assert.doesNotMatch(rule('.client-gateway-summary'), /background|border|box-shadow/);
|
||||
@@ -92,11 +96,13 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
|
||||
assert.match(responsive, /max-height:\s*none/);
|
||||
assert.match(responsive, /overflow-y:\s*visible/);
|
||||
assert.match(responsive, /\.harbor-brand,[\s\S]*position:\s*absolute/);
|
||||
assert.match(responsive, /\.client-power-control > \.harbor-brand \{[\s\S]*scale\(1\.55\)/);
|
||||
assert.match(responsive, /\.client-inline-error\.is-connection,[\s\S]*position:\s*static/);
|
||||
|
||||
assert.match(rule('.client-duration-toggle'), /width:\s*min\(290px, 100%\)/);
|
||||
assert.match(mobile, /\.client-server-toolbar \{[\s\S]*grid-template-rows:\s*44px 44px/);
|
||||
assert.match(mobile, /\.client-form-content \{[\s\S]*gap:\s*24px/);
|
||||
assert.match(mobile, /\.client-power-control > \.harbor-brand \{[\s\S]*scale\(1\.35\)/);
|
||||
assert.match(mobile, /\.client-server-check \{[\s\S]*width:\s*44px/);
|
||||
assert.match(styles, /\.client-instructions\s*\{[\s\S]*width:\s*min\(470px, 100vw\)/);
|
||||
assert.match(styles, /\.client-local-rules\s*\{[\s\S]*width:\s*min\(480px, 100vw\)/);
|
||||
|
||||
@@ -36,26 +36,26 @@ const expectedImports = [
|
||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const acceptedLedger = {
|
||||
counts: {
|
||||
cascadeEdges: 780,
|
||||
cascadeEdges: 797,
|
||||
customProperties: 31,
|
||||
declarations: 2885,
|
||||
declarations: 2900,
|
||||
important: 0,
|
||||
keyframes: 48,
|
||||
media: 10,
|
||||
rules: 861,
|
||||
rules: 864,
|
||||
variableReferences: 339,
|
||||
},
|
||||
hashes: {
|
||||
cascadeEdges: '7d47b1c26e06748e4532ce9ba7229e9a9bab4058b9c615129ce668aa7e906714',
|
||||
cascadeEdges: '55bd3926b5da1a9ccc6dee5f3eb5cf2c111183e0410edcc085545e5a35c0b229',
|
||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||
declarations: 'ea140d4e00166daba7dc92fabb2ef63afd2ed3cb9d6c0dadeaf346bbf1eddc32',
|
||||
declarations: '67441a07783c586844638db628ab401bbf52bdb1eaf5dbec20e65bebff6faac1',
|
||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
||||
keyframes: '853c54c05759d9db27bea891254913e1651b1f601059ad9e3e2baa2c55ef1b2b',
|
||||
ruleDeclarationSequences: '1a231e3068345d08f1670b64e5b2db91abbdb7083f164d3e96f073d4ae561bdb',
|
||||
selectors: '824d578e36c3ae25a39bc686494635da353ea45996633eed01f93132dfb4dabd',
|
||||
ruleDeclarationSequences: '75f73d80e2c00e0575974b3cfdef3d27cd509e598f09250223e813f107e0169e',
|
||||
selectors: '7e87d3d05fa525ad2414fbd58bd128a1b30be5c4b01a27151c03c9c2692d6384',
|
||||
variableReferences: 'da4298e47c6249c3cfa12168d3670b0ec9c92c0fca7033ba968ea70598753a65',
|
||||
witnesses: '6d498098d34d0b9fe399e457ece265456d99b4351a46a534e1b4fe5e4620e90f',
|
||||
witnesses: '855149a346b32cf84595b8aabf3acef047d7366ee87aea79ab66165042e3a001',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||
assert.deepEqual(assets, ['index-ZmHjygJv.css']);
|
||||
assert.deepEqual(assets, ['index-CKZGCKzI.css']);
|
||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||
assert.equal(built.byteLength, 107497);
|
||||
assert.equal(sha256(built), '40412db73b9c6cd2f2f49f4a2b6cdf2130253a12f4d0d0e4a37277017933421c');
|
||||
assert.equal(built.byteLength, 107832);
|
||||
assert.equal(sha256(built), 'fee93089544bccf45203d53329d54ae10092e3bd93f0cd9b83dccda2202406ef');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user