Add runtime version reporting and display
This commit is contained in:
@@ -20,6 +20,7 @@ function App() {
|
||||
const [subscriptionUrl, setSubscriptionUrl] = useState('');
|
||||
const [operations, setOperations] = useState({});
|
||||
const [error, setError] = useState(null);
|
||||
const [versionInfo, setVersionInfo] = useState(null);
|
||||
const pollGeneration = useRef(0);
|
||||
const operationRegistry = useRef(null);
|
||||
if (!operationRegistry.current) {
|
||||
@@ -56,6 +57,17 @@ function App() {
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
api.version().then((info) => {
|
||||
if (!cancelled) setVersionInfo(info);
|
||||
}).catch((requestError) => {
|
||||
console.warn(`[version] Не удалось получить runtime-версию: ${requestError.message}`);
|
||||
if (!cancelled) setVersionInfo(null);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state?.mode) return;
|
||||
const isGateway = state.mode === 'gateway';
|
||||
@@ -129,6 +141,7 @@ function App() {
|
||||
<main className="app-main">
|
||||
<ClientOverviewPage
|
||||
state={previewReady ? { ...state, mode: 'client', hasSubscription: true, subscriptionHost: 'harbor.example', selectedTag: 'Amsterdam', proxyPort: 8082 } : state}
|
||||
versionInfo={versionInfo}
|
||||
operations={operations}
|
||||
error={error}
|
||||
subscriptionUrl={subscriptionUrl}
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function request(url, options = {}, fetchImpl = fetch) {
|
||||
|
||||
export const api = {
|
||||
state: () => request('/api/state'),
|
||||
version: () => request('/api/version'),
|
||||
subscription: {
|
||||
validate: (url, signal) => request('/api/subscription/validate', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
import { formatBytes } from '../utils/format.js';
|
||||
import { instructionBlocks } from '../instructions.js';
|
||||
import { createLatestRequest, operationBlocked } from '../state/operations.js';
|
||||
import {
|
||||
HARBOR_VERSIONS,
|
||||
parseVersion,
|
||||
versionCompatibility,
|
||||
} from '../../shared/versions.js';
|
||||
|
||||
const SUBSCRIPTION_REVEAL_DELAY_MS = 1350;
|
||||
const DURATION_MODE_STORAGE_KEY = 'harbor-duration-mode';
|
||||
@@ -21,6 +26,96 @@ function CloudTooltip({ children }) {
|
||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
||||
}
|
||||
|
||||
const VERSION_PARTS = [
|
||||
['major', 'Major'],
|
||||
['minor', 'Minor'],
|
||||
['hotfix', 'Hotfix'],
|
||||
];
|
||||
|
||||
function VersionBadge({ code, component, componentKey, version, singBox, incompatible = false }) {
|
||||
const parsed = parseVersion(version);
|
||||
const values = parsed ? VERSION_PARTS.map(([key]) => parsed[key]) : ['–', '–', '–'];
|
||||
|
||||
function description(key) {
|
||||
if (key === 'major') {
|
||||
return 'Общий уровень совместимости Harbor. При его изменении обновляются Mac и вся Gateway-инфраструктура.';
|
||||
}
|
||||
if (key === 'minor' && componentKey === 'macClient') {
|
||||
return 'Линия Mac-клиента. Gateway может менять minor без обязательного обновления Mac.';
|
||||
}
|
||||
if (key === 'minor') {
|
||||
return 'Линия Gateway. Gateway client и backend должны совпадать по major.minor.';
|
||||
}
|
||||
return `Совместимое исправление только компонента ${component}; hotfix может обновляться независимо.`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`harbor-version${incompatible ? ' is-incompatible' : ''}`}>
|
||||
<span className="harbor-version-code" aria-hidden="true">{code}</span>
|
||||
<span className="harbor-version-number" aria-label={`${component} ${version || 'не определена'}`}>
|
||||
{VERSION_PARTS.map(([key, label], index) => {
|
||||
const tooltipId = `harbor-version-${componentKey}-${key}`;
|
||||
return <React.Fragment key={key}>
|
||||
{index > 0 && <span className="harbor-version-dot" aria-hidden="true">.</span>}
|
||||
<span
|
||||
className="harbor-version-part"
|
||||
tabIndex="0"
|
||||
aria-describedby={tooltipId}
|
||||
>
|
||||
{values[index]}
|
||||
<span className="harbor-version-tooltip" id={tooltipId} role="tooltip">
|
||||
<strong>{component} · {label} {values[index]}</strong>
|
||||
<span>{description(key)}</span>
|
||||
{singBox && <small>Runtime: sing-box {singBox}</small>}
|
||||
{incompatible && <small className="is-warning">Версии Gateway несовместимы.</small>}
|
||||
</span>
|
||||
</span>
|
||||
</React.Fragment>;
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionDisplay({ isGateway, versionInfo }) {
|
||||
const runtimeSingBox = versionInfo?.runtime?.singBox;
|
||||
if (!isGateway) {
|
||||
return <aside className="harbor-versions" aria-label="Версия Harbor">
|
||||
<VersionBadge
|
||||
code="M"
|
||||
component="Mac client"
|
||||
componentKey="macClient"
|
||||
version={versionInfo?.components?.macClient || HARBOR_VERSIONS.macClient}
|
||||
singBox={runtimeSingBox}
|
||||
/>
|
||||
</aside>;
|
||||
}
|
||||
|
||||
const backendVersion = versionInfo?.components?.gatewayBackend;
|
||||
const compatibility = backendVersion && versionCompatibility({
|
||||
...HARBOR_VERSIONS,
|
||||
gatewayBackend: backendVersion,
|
||||
});
|
||||
const incompatible = compatibility && !compatibility.compatible;
|
||||
return <aside className="harbor-versions" aria-label="Версии Harbor Gateway">
|
||||
<VersionBadge
|
||||
code="C"
|
||||
component="Gateway client"
|
||||
componentKey="gatewayClient"
|
||||
version={HARBOR_VERSIONS.gatewayClient}
|
||||
incompatible={incompatible}
|
||||
/>
|
||||
<VersionBadge
|
||||
code="B"
|
||||
component="Gateway backend"
|
||||
componentKey="gatewayBackend"
|
||||
version={backendVersion}
|
||||
singBox={runtimeSingBox}
|
||||
incompatible={incompatible}
|
||||
/>
|
||||
</aside>;
|
||||
}
|
||||
|
||||
function InlineError({ error, context }) {
|
||||
if (!error || error.context !== context) return null;
|
||||
return (
|
||||
@@ -198,6 +293,7 @@ function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, blocked, onSe
|
||||
|
||||
export function ClientOverviewPage({
|
||||
state,
|
||||
versionInfo,
|
||||
operations = {},
|
||||
error,
|
||||
subscriptionUrl,
|
||||
@@ -553,6 +649,7 @@ export function ClientOverviewPage({
|
||||
|
||||
return (
|
||||
<div className={`client-shell${hasSubscription ? '' : ' is-first-run'}${showIntro && !hasSubscription ? ' is-intro' : ''}`}>
|
||||
<VersionDisplay isGateway={isGateway} versionInfo={versionInfo} />
|
||||
<HarborBrand
|
||||
isGateway={isGateway}
|
||||
gatewayAvailable={gatewayAvailable}
|
||||
|
||||
@@ -192,6 +192,118 @@ p {
|
||||
--client-accent: var(--harbor-gateway);
|
||||
}
|
||||
|
||||
.harbor-versions {
|
||||
position: fixed;
|
||||
right: max(14px, env(safe-area-inset-right));
|
||||
bottom: max(12px, env(safe-area-inset-bottom));
|
||||
z-index: 40;
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 2px;
|
||||
color: var(--client-muted);
|
||||
font: 600 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.harbor-version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
opacity: 0.56;
|
||||
transition: color 240ms ease, opacity 240ms ease;
|
||||
}
|
||||
|
||||
.harbor-version:focus-within,
|
||||
.harbor-version:hover {
|
||||
color: var(--client-text);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.harbor-version.is-incompatible {
|
||||
color: oklch(0.68 0.15 28);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.harbor-version-code {
|
||||
width: 1.3ch;
|
||||
color: var(--client-accent);
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.harbor-version-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.harbor-version-part {
|
||||
position: relative;
|
||||
min-width: 1ch;
|
||||
padding: 3px 1px;
|
||||
border-radius: 4px;
|
||||
cursor: help;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.harbor-version-part:hover,
|
||||
.harbor-version-part:focus-visible {
|
||||
outline: none;
|
||||
color: var(--client-accent);
|
||||
text-shadow: 0 0 8px color-mix(in oklch, var(--client-accent) 48%, transparent);
|
||||
}
|
||||
|
||||
.harbor-version-part:focus-visible {
|
||||
box-shadow: 0 0 0 1px var(--client-accent);
|
||||
}
|
||||
|
||||
.harbor-version-tooltip {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: calc(100% + 7px);
|
||||
width: min(250px, calc(100vw - 28px));
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in oklch, var(--client-panel) 84%, transparent);
|
||||
box-shadow: 0 9px 30px oklch(0.08 0.015 145 / 0.16);
|
||||
backdrop-filter: blur(12px) saturate(0.9);
|
||||
color: var(--client-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
transform: translateY(3px);
|
||||
transition: opacity 90ms ease, filter 120ms ease, transform 140ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s 140ms;
|
||||
}
|
||||
|
||||
.harbor-version-tooltip strong {
|
||||
color: var(--client-text);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.harbor-version-tooltip small {
|
||||
color: var(--client-accent);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.harbor-version-tooltip .is-warning {
|
||||
color: oklch(0.68 0.15 28);
|
||||
}
|
||||
|
||||
.harbor-version-part:hover .harbor-version-tooltip,
|
||||
.harbor-version-part:focus-visible .harbor-version-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
filter: blur(0);
|
||||
transform: translateY(0);
|
||||
transition-delay: 20ms, 20ms, 20ms, 0s;
|
||||
}
|
||||
|
||||
.harbor-brand {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
@@ -2178,6 +2290,11 @@ p {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.harbor-version,
|
||||
.harbor-version-tooltip {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.client-state-copy h2,
|
||||
.client-connection-title > span,
|
||||
.client-state-detail > * {
|
||||
|
||||
Reference in New Issue
Block a user