Reposition Gateway traffic summary
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.18.0',
|
macClient: '0.18.1',
|
||||||
gatewayClient: '0.19.0',
|
gatewayClient: '0.19.1',
|
||||||
gatewayBackend: '0.19.0',
|
gatewayBackend: '0.19.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1338,31 +1338,6 @@ export function ClientOverviewPage({
|
|||||||
</button>
|
</button>
|
||||||
</nav>}
|
</nav>}
|
||||||
<main className={`client-panel${showPower ? '' : ' is-setup'}${!isGateway && hasSubscription ? ' has-subscription' : ''}${isGateway ? ' is-gateway-home' : ''}`}>
|
<main className={`client-panel${showPower ? '' : ' is-setup'}${!isGateway && hasSubscription ? ' has-subscription' : ''}${isGateway ? ' is-gateway-home' : ''}`}>
|
||||||
{isGateway && <section className="client-gateway-summary" aria-labelledby="gateway-summary-title">
|
|
||||||
<span className="client-gateway-summary-kicker">Сейчас</span>
|
|
||||||
<h2 id="gateway-summary-title">
|
|
||||||
{appliedServer?.label || 'VPN-сервер не используется'}
|
|
||||||
</h2>
|
|
||||||
<div className="client-gateway-route-slot" role="status" aria-live="polite">
|
|
||||||
{switchingServer && <span>Переключаем на {desiredServer.label}</span>}
|
|
||||||
</div>
|
|
||||||
<div className="client-gateway-traffic-heading">
|
|
||||||
<span>Учтено Harbor</span>
|
|
||||||
<strong>{formatByteString(globalTraffic?.totalBytes || '0')}</strong>
|
|
||||||
</div>
|
|
||||||
<div className="client-gateway-traffic-chart">
|
|
||||||
<TrafficChart
|
|
||||||
samples={globalTraffic?.history || []}
|
|
||||||
capacity={deviceSnapshot?.trafficHistoryCapacity || 120}
|
|
||||||
routeLabel="Gateway"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={`client-gateway-traffic-freshness${trafficSourceError ? ' is-stale' : ''}`} role="status" aria-live="polite">
|
|
||||||
{trafficSourceError
|
|
||||||
? `Трафик не обновляется · последние данные ${trafficFreshness}`
|
|
||||||
: globalTraffic?.observedAt ? `Обновлено ${trafficFreshness}` : 'Ожидаем первые данные трафика'}
|
|
||||||
</div>
|
|
||||||
</section>}
|
|
||||||
{showPower && (
|
{showPower && (
|
||||||
<section className="client-power-section" aria-labelledby="connection-title">
|
<section className="client-power-section" aria-labelledby="connection-title">
|
||||||
{isGateway ? <span
|
{isGateway ? <span
|
||||||
@@ -1390,6 +1365,15 @@ export function ClientOverviewPage({
|
|||||||
<span className={connected && !gatewayDirect ? 'is-active' : ''} aria-hidden="true">VPN включён</span>
|
<span className={connected && !gatewayDirect ? 'is-active' : ''} aria-hidden="true">VPN включён</span>
|
||||||
<span className={connected && gatewayDirect ? 'is-active' : ''} aria-hidden="true">Gateway подключён</span>
|
<span className={connected && gatewayDirect ? 'is-active' : ''} aria-hidden="true">Gateway подключён</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
{isGateway && <div className="client-gateway-route-summary" aria-labelledby="gateway-summary-title">
|
||||||
|
<span className="client-gateway-summary-kicker">Сейчас</span>
|
||||||
|
<strong id="gateway-summary-title">
|
||||||
|
{appliedServer?.label || 'VPN-сервер не используется'}
|
||||||
|
</strong>
|
||||||
|
<div className="client-gateway-route-slot">
|
||||||
|
{switchingServer && <span>Переключаем на {desiredServer.label}</span>}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
<div className="client-state-detail">
|
<div className="client-state-detail">
|
||||||
{connected ? (
|
{connected ? (
|
||||||
<button
|
<button
|
||||||
@@ -1442,6 +1426,25 @@ export function ClientOverviewPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isGateway && <section className="client-gateway-summary" aria-label="Общий трафик Harbor">
|
||||||
|
<div className="client-gateway-traffic-heading">
|
||||||
|
<span>Учтено Harbor</span>
|
||||||
|
<strong>{formatByteString(globalTraffic?.totalBytes || '0')}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="client-gateway-traffic-chart">
|
||||||
|
<TrafficChart
|
||||||
|
samples={globalTraffic?.history || []}
|
||||||
|
capacity={deviceSnapshot?.trafficHistoryCapacity || 120}
|
||||||
|
routeLabel="Gateway"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`client-gateway-traffic-freshness${trafficSourceError ? ' is-stale' : ''}`} role="status" aria-live="polite">
|
||||||
|
{trafficSourceError
|
||||||
|
? `Трафик не обновляется · последние данные ${trafficFreshness}`
|
||||||
|
: globalTraffic?.observedAt ? `Обновлено ${trafficFreshness}` : 'Ожидаем первые данные трафика'}
|
||||||
|
</div>
|
||||||
|
</section>}
|
||||||
|
|
||||||
<section className={`client-proxies${isGateway ? ' is-gateway' : ''}`} aria-label={isGateway ? 'Gateway и Gateway Proxy' : 'Локальный прокси'}>
|
<section className={`client-proxies${isGateway ? ' is-gateway' : ''}`} aria-label={isGateway ? 'Gateway и Gateway Proxy' : 'Локальный прокси'}>
|
||||||
<div className="client-access-point">
|
<div className="client-access-point">
|
||||||
{!isGateway && (
|
{!isGateway && (
|
||||||
|
|||||||
+28
-26
@@ -2616,15 +2616,10 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-summary {
|
.client-gateway-summary {
|
||||||
grid-column: 1;
|
width: min(300px, 100%);
|
||||||
align-self: center;
|
|
||||||
justify-self: end;
|
|
||||||
width: min(360px, 100%);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding-right: clamp(20px, 4vw, 64px);
|
text-align: center;
|
||||||
text-align: left;
|
|
||||||
animation: client-power-arrive 850ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-summary-kicker,
|
.client-gateway-summary-kicker,
|
||||||
@@ -2636,22 +2631,34 @@ p {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-summary h2 {
|
.client-gateway-route-summary {
|
||||||
min-height: 29px;
|
min-height: 54px;
|
||||||
margin: 0;
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-gateway-route-summary strong {
|
||||||
|
max-width: min(300px, 100%);
|
||||||
|
min-height: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--client-text);
|
color: var(--client-text);
|
||||||
font-size: 22px;
|
font-size: 15px;
|
||||||
letter-spacing: -0.045em;
|
letter-spacing: -0.035em;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-route-slot {
|
.client-gateway-route-slot {
|
||||||
min-height: 18px;
|
width: 100%;
|
||||||
|
min-height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-heading {
|
.client-gateway-traffic-heading {
|
||||||
@@ -2659,7 +2666,6 @@ p {
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-heading strong {
|
.client-gateway-traffic-heading strong {
|
||||||
@@ -2671,18 +2677,18 @@ p {
|
|||||||
|
|
||||||
.client-gateway-traffic-chart {
|
.client-gateway-traffic-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 118px;
|
min-height: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-chart .client-device-traffic-chart {
|
.client-gateway-traffic-chart .client-device-traffic-chart {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
grid-row: auto;
|
grid-row: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 118px;
|
height: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-traffic-freshness {
|
.client-gateway-traffic-freshness {
|
||||||
min-height: 28px;
|
min-height: 26px;
|
||||||
color: var(--client-muted);
|
color: var(--client-muted);
|
||||||
font-size: 8.5px;
|
font-size: 8.5px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -2764,6 +2770,10 @@ p {
|
|||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-panel.is-gateway-home .client-state-copy {
|
||||||
|
min-height: 136px;
|
||||||
|
}
|
||||||
|
|
||||||
.client-power-control {
|
.client-power-control {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
@@ -4532,7 +4542,6 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-power-section,
|
.client-power-section,
|
||||||
.client-gateway-summary,
|
|
||||||
.client-form,
|
.client-form,
|
||||||
.client-panel.is-setup .client-form {
|
.client-panel.is-setup .client-form {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
@@ -4544,12 +4553,6 @@ p {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-gateway-summary {
|
|
||||||
order: 2;
|
|
||||||
justify-self: center;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form {
|
.client-form {
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
@@ -5176,7 +5179,6 @@ p {
|
|||||||
.client-subscription-refresh,
|
.client-subscription-refresh,
|
||||||
.client-subscription-delete,
|
.client-subscription-delete,
|
||||||
.client-power-section,
|
.client-power-section,
|
||||||
.client-gateway-summary,
|
|
||||||
.client-gateway-traffic-chart,
|
.client-gateway-traffic-chart,
|
||||||
.client-gateway-traffic-freshness,
|
.client-gateway-traffic-freshness,
|
||||||
.client-subscription-drawer,
|
.client-subscription-drawer,
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
|
|||||||
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
|
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
|
||||||
assert.match(overview, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
|
assert.match(overview, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
|
||||||
assert.match(overview, /samples=\{globalTraffic\?\.history \|\| \[\]\}[\s\S]*capacity=\{deviceSnapshot\?\.trafficHistoryCapacity \|\| 120\}[\s\S]*routeLabel="Gateway"/);
|
assert.match(overview, /samples=\{globalTraffic\?\.history \|\| \[\]\}[\s\S]*capacity=\{deviceSnapshot\?\.trafficHistoryCapacity \|\| 120\}[\s\S]*routeLabel="Gateway"/);
|
||||||
|
assert.match(overview, /<section className="client-power-section"[\s\S]*client-connection-title[\s\S]*client-gateway-route-summary[\s\S]*client-state-detail[\s\S]*<section className="client-gateway-summary"[\s\S]*client-proxies/);
|
||||||
assert.doesNotMatch(overview, /<TrafficChart[\s\S]{0,240}scale=/);
|
assert.doesNotMatch(overview, /<TrafficChart[\s\S]{0,240}scale=/);
|
||||||
assert.match(overview, /deviceStatus === 'error' \? deviceError : null/);
|
assert.match(overview, /deviceStatus === 'error' \? deviceError : null/);
|
||||||
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.singboxStartedAt\)\) return undefined/);
|
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.singboxStartedAt\)\) return undefined/);
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ test('desktop layout keeps the power control on a symmetric center axis', () =>
|
|||||||
assert.match(styles, /\.client-panel\.has-subscription \{[\s\S]*transform:\s*translateY\(-9vh\)/);
|
assert.match(styles, /\.client-panel\.has-subscription \{[\s\S]*transform:\s*translateY\(-9vh\)/);
|
||||||
assert.match(styles, /\.client-panel\.has-subscription \.client-power-section \{[\s\S]*height:\s*var\(--client-work-height\);[\s\S]*padding-top:\s*var\(--client-power-top\)/);
|
assert.match(styles, /\.client-panel\.has-subscription \.client-power-section \{[\s\S]*height:\s*var\(--client-work-height\);[\s\S]*padding-top:\s*var\(--client-power-top\)/);
|
||||||
assert.match(styles, /\.client-panel\.has-subscription \.client-form-content \{[\s\S]*padding-top:\s*var\(--client-power-top\)/);
|
assert.match(styles, /\.client-panel\.has-subscription \.client-form-content \{[\s\S]*padding-top:\s*var\(--client-power-top\)/);
|
||||||
assert.match(rule('.client-gateway-summary'), /grid-column:\s*1/);
|
assert.match(rule('.client-gateway-summary'), /width:\s*min\(300px, 100%\)/);
|
||||||
assert.match(rule('.client-gateway-summary'), /justify-self:\s*end/);
|
assert.doesNotMatch(rule('.client-gateway-summary'), /grid-column|justify-self|padding-right/);
|
||||||
|
assert.match(rule('.client-gateway-route-slot'), /min-height:\s*16px[\s\S]*overflow:\s*hidden[\s\S]*text-overflow:\s*ellipsis[\s\S]*white-space:\s*nowrap/);
|
||||||
assert.match(component, /client-panel\$\{showPower \? '' : ' is-setup'\}[\s\S]*is-gateway-home/);
|
assert.match(component, /client-panel\$\{showPower \? '' : ' is-setup'\}[\s\S]*is-gateway-home/);
|
||||||
assert.match(component, /const showPower = isGateway \|\| \(hasSubscription && Boolean\(selectedServerId\)\)/);
|
assert.match(component, /const showPower = isGateway \|\| \(hasSubscription && Boolean\(selectedServerId\)\)/);
|
||||||
});
|
});
|
||||||
@@ -72,9 +73,8 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
|
|||||||
|
|
||||||
assert.match(responsive, /grid-template-columns:\s*minmax\(0, 1fr\)/);
|
assert.match(responsive, /grid-template-columns:\s*minmax\(0, 1fr\)/);
|
||||||
assert.match(responsive, /\.client-power-section,[\s\S]*\.client-form,[\s\S]*grid-column:\s*1/);
|
assert.match(responsive, /\.client-power-section,[\s\S]*\.client-form,[\s\S]*grid-column:\s*1/);
|
||||||
assert.match(responsive, /\.client-gateway-summary/);
|
|
||||||
assert.match(responsive, /\.client-power-section \{[\s\S]*order:\s*1/);
|
assert.match(responsive, /\.client-power-section \{[\s\S]*order:\s*1/);
|
||||||
assert.match(responsive, /\.client-gateway-summary \{[\s\S]*order:\s*2/);
|
assert.doesNotMatch(responsive, /\.client-gateway-summary/);
|
||||||
assert.match(responsive, /width:\s*min\(100%, 380px\)/);
|
assert.match(responsive, /width:\s*min\(100%, 380px\)/);
|
||||||
assert.match(responsive, /max-height:\s*none/);
|
assert.match(responsive, /max-height:\s*none/);
|
||||||
assert.match(responsive, /overflow-y:\s*visible/);
|
assert.match(responsive, /overflow-y:\s*visible/);
|
||||||
|
|||||||
Reference in New Issue
Block a user