Update Harbor client and gateway integration workflows
This commit is contained in:
@@ -47,6 +47,13 @@ import {
|
||||
InstructionsToggle,
|
||||
useInstructionsFeature,
|
||||
} from '../features/instructions/index.js';
|
||||
import { FailoverPanel, FailoverToggle, useFailoverFeature } from '../features/failover/index.js';
|
||||
import {
|
||||
ActivityJournalPanel,
|
||||
ActivityJournalToggle,
|
||||
useActivityJournalFeature,
|
||||
} from '../features/activity-journal/index.js';
|
||||
import type { FailoverPolicy } from '../../shared/failover.js';
|
||||
import {
|
||||
HARBOR_VERSIONS,
|
||||
parseVersion,
|
||||
@@ -65,9 +72,33 @@ const VERSION_PARTS = [
|
||||
] as const;
|
||||
|
||||
const DRAWER_SWITCH_MS = 620;
|
||||
const DRAWER_ORDER = ['subscription', 'instructions', 'devices', 'diagnostics', 'routing'] as const;
|
||||
const DRAWER_ORDER = ['subscription', 'failover', 'instructions', 'devices', 'diagnostics', 'routing', 'journal'] as const;
|
||||
type DrawerKey = typeof DRAWER_ORDER[number];
|
||||
|
||||
const failoverReasonLabel = (reason: string | null) => ({
|
||||
'primary-healthy': 'основной работает',
|
||||
'health-unknown': 'ожидаем проверку',
|
||||
'failure-window': 'подтверждаем сбой',
|
||||
'reserve-not-healthy': 'резерв не подтверждён',
|
||||
'both-unhealthy': 'оба канала недоступны',
|
||||
'primary-not-recovered': 'основной восстанавливается',
|
||||
'recovery-hold': 'проверяем стабильность',
|
||||
'activity-unknown': 'активность неизвестна',
|
||||
'active-traffic': 'ждём завершения работы',
|
||||
'quiet-window': 'проверяем тишину',
|
||||
'primary-failed': 'основной недоступен',
|
||||
'primary-recovered': 'основной восстановился',
|
||||
'pending-activation': 'изменения ожидают запуска',
|
||||
'vpn-stopped': 'VPN выключен',
|
||||
paused: 'автоматика на паузе',
|
||||
disabled: 'резерв выключен',
|
||||
'switch-failed': 'не удалось переключить',
|
||||
'selector-unknown': 'текущий канал неизвестен',
|
||||
'reconcile-failed': 'мониторинг временно недоступен',
|
||||
'revalidation-required': 'условия проверяются заново',
|
||||
'manual-check': 'оба канала проверены',
|
||||
}[reason || ''] || 'наблюдение');
|
||||
|
||||
interface UiError {
|
||||
context?: string;
|
||||
profileId?: string;
|
||||
@@ -93,6 +124,7 @@ interface ComponentActions {
|
||||
setDevicePolicy: (id: string, mode: 'vpn' | 'direct', expectedRevision: number) => Promise<unknown>;
|
||||
pingServers: (profileId: string, ids: string[]) => Promise<unknown>;
|
||||
runConnectivityDiagnostics: (target?: unknown) => Promise<unknown>;
|
||||
loadActivityJournal: (cursor?: string | null) => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface ClientViewState extends StateSnapshot {
|
||||
@@ -119,6 +151,10 @@ interface ClientOverviewPageProps {
|
||||
onSetGatewayAuto: (enabled: boolean) => Promise<unknown>;
|
||||
onSaveRouteRules: (rules: RouteRule[], expectedRevision: number) => Promise<unknown>;
|
||||
onUpdateDiagnosticsSettings: (settings: unknown) => Promise<unknown>;
|
||||
onSaveFailover: (policy: FailoverPolicy) => Promise<unknown>;
|
||||
onPauseFailover: (paused: boolean) => Promise<unknown>;
|
||||
onSwitchFailover: (role: 'primary' | 'reserve') => Promise<unknown>;
|
||||
onCheckFailover: () => Promise<unknown>;
|
||||
onDismissError: () => void;
|
||||
}
|
||||
|
||||
@@ -252,6 +288,7 @@ const operationProgress: Partial<Record<keyof OperationRegistrySnapshot, readonl
|
||||
profileRefresh: ['subscription', 'Обновляем подписку…'],
|
||||
profileDelete: ['subscription', 'Удаляем подписку…'],
|
||||
routeRules: ['routing', 'Применяем локальные правила…'],
|
||||
failover: ['failover', 'Применяем настройки резерва…'],
|
||||
};
|
||||
|
||||
const canonicalOperationKeys: Record<string, OperationKey> = {
|
||||
@@ -266,6 +303,10 @@ const canonicalOperationKeys: Record<string, OperationKey> = {
|
||||
'profile-delete': 'profileDelete',
|
||||
'gateway-auto': 'gatewayAuto',
|
||||
'route-rules': 'routeRules',
|
||||
'failover-save': 'failover',
|
||||
'failover-pause': 'failover',
|
||||
'failover-resume': 'failover',
|
||||
'failover-switch': 'failover',
|
||||
'subscription-import': 'profileAdd',
|
||||
'subscription-refresh': 'profileRefresh',
|
||||
'subscription-forget': 'profileDelete',
|
||||
@@ -425,6 +466,10 @@ export function ClientOverviewPage({
|
||||
onSetGatewayAuto,
|
||||
onSaveRouteRules,
|
||||
onUpdateDiagnosticsSettings,
|
||||
onSaveFailover,
|
||||
onPauseFailover,
|
||||
onSwitchFailover,
|
||||
onCheckFailover,
|
||||
onDismissError,
|
||||
}: ClientOverviewPageProps) {
|
||||
const isGateway = state?.mode === 'gateway';
|
||||
@@ -533,6 +578,8 @@ export function ClientOverviewPage({
|
||||
port: state?.clientRuntime?.proxyPort || (isGateway ? 8080 : 8082),
|
||||
controlHost,
|
||||
});
|
||||
const failoverFeature = useFailoverFeature();
|
||||
const activityJournalFeature = useActivityJournalFeature();
|
||||
const diagnosticsAvailable = hasSubscription;
|
||||
const drawerControls = {
|
||||
subscription: {
|
||||
@@ -541,6 +588,12 @@ export function ClientOverviewPage({
|
||||
show: subscriptionFeature.toggle,
|
||||
close: subscriptionFeature.close,
|
||||
},
|
||||
failover: {
|
||||
isOpen: failoverFeature.isOpen,
|
||||
panelRef: failoverFeature.panelRef,
|
||||
show: failoverFeature.toggle,
|
||||
close: failoverFeature.close,
|
||||
},
|
||||
instructions: {
|
||||
isOpen: instructionsFeature.isOpen,
|
||||
panelRef: instructionsFeature.panelRef,
|
||||
@@ -565,10 +618,16 @@ export function ClientOverviewPage({
|
||||
show: routingFeature.open,
|
||||
close: routingFeature.forceClose,
|
||||
},
|
||||
journal: {
|
||||
isOpen: activityJournalFeature.isOpen,
|
||||
panelRef: activityJournalFeature.panelRef,
|
||||
show: activityJournalFeature.toggle,
|
||||
close: activityJournalFeature.close,
|
||||
},
|
||||
};
|
||||
const drawerOrder = isGateway
|
||||
? DRAWER_ORDER
|
||||
: DRAWER_ORDER.filter((drawer) => drawer !== 'devices');
|
||||
: DRAWER_ORDER.filter((drawer) => !['devices', 'failover', 'journal'].includes(drawer));
|
||||
const activeRailDrawer = drawerSwitchTarget && drawerControls[drawerSwitchTarget].isOpen
|
||||
? drawerSwitchTarget
|
||||
: drawerOrder.find((drawer) => drawerControls[drawer].isOpen) || null;
|
||||
@@ -587,6 +646,8 @@ export function ClientOverviewPage({
|
||||
instructionsFeature.close();
|
||||
devicesFeature.close();
|
||||
diagnosticsFeature.close();
|
||||
failoverFeature.close();
|
||||
activityJournalFeature.close();
|
||||
}
|
||||
}, [hasSubscription, isGateway]);
|
||||
|
||||
@@ -656,6 +717,7 @@ export function ClientOverviewPage({
|
||||
routingFeature.requestClose();
|
||||
return;
|
||||
}
|
||||
if (current === 'failover' && !failoverFeature.beforeCloseRef.current()) return;
|
||||
if (!current) {
|
||||
drawerControls[target].show();
|
||||
return;
|
||||
@@ -733,6 +795,13 @@ export function ClientOverviewPage({
|
||||
: switchingServer && operationProfile && operationServer
|
||||
? `Переключаем на ${subscriptionDomain(operationProfile.subscription.host)} · ${operationServer.label}`
|
||||
: '';
|
||||
const failoverIdentity = isGateway && state.failover.enabled
|
||||
? `${state.failover.currentRole === 'reserve'
|
||||
? 'Резервный канал'
|
||||
: state.failover.currentRole === 'primary'
|
||||
? 'Основной канал'
|
||||
: 'Текущий канал вне резервной пары'} · ${failoverReasonLabel(state.failover.reason)}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -752,6 +821,11 @@ export function ClientOverviewPage({
|
||||
open={activeRailDrawer === 'subscription'}
|
||||
onToggle={() => switchDrawer('subscription')}
|
||||
/>
|
||||
{isGateway && <FailoverToggle
|
||||
feature={failoverFeature}
|
||||
open={activeRailDrawer === 'failover'}
|
||||
onToggle={() => switchDrawer('failover')}
|
||||
/>}
|
||||
<InstructionsToggle
|
||||
feature={instructionsFeature}
|
||||
open={activeRailDrawer === 'instructions'}
|
||||
@@ -775,6 +849,11 @@ export function ClientOverviewPage({
|
||||
hasSubscription={hasSubscription}
|
||||
onOpen={() => switchDrawer('routing')}
|
||||
/>
|
||||
{isGateway && <ActivityJournalToggle
|
||||
feature={activityJournalFeature}
|
||||
open={activeRailDrawer === 'journal'}
|
||||
onToggle={() => switchDrawer('journal')}
|
||||
/>}
|
||||
</nav>}
|
||||
<main className={`client-panel${showPower ? '' : ' is-setup'}${!isGateway && hasSubscription ? ' has-subscription' : ''}${isGateway && hasSubscription ? ' is-gateway-home' : ''}`}>
|
||||
<ConnectionPanel
|
||||
@@ -807,7 +886,7 @@ export function ClientOverviewPage({
|
||||
blocked={connectionBlocked}
|
||||
onRestart={onRestart}
|
||||
/>}
|
||||
serverSlot={<AppliedIdentity identity={mainIdentity} operation={switchIdentity} />}
|
||||
serverSlot={<AppliedIdentity identity={mainIdentity} operation={switchIdentity || failoverIdentity} />}
|
||||
statusSlot={<>
|
||||
<InlineError error={error} context="connection" />
|
||||
<InlineProgress operations={visibleOperations} context="connection" />
|
||||
@@ -857,6 +936,22 @@ export function ClientOverviewPage({
|
||||
<InlineProgress operations={visibleOperations} context="routing" />
|
||||
</>}
|
||||
/>}
|
||||
{isGateway && hasSubscription && <FailoverPanel
|
||||
feature={failoverFeature}
|
||||
snapshot={state.failover}
|
||||
profiles={profiles}
|
||||
diagnostics={state.diagnostics}
|
||||
blocked={operationBlocked(visibleOperations, 'failover')}
|
||||
onSave={onSaveFailover}
|
||||
onPause={onPauseFailover}
|
||||
onSwitch={onSwitchFailover}
|
||||
onCheck={onCheckFailover}
|
||||
onUpdateDiagnostics={onUpdateDiagnosticsSettings}
|
||||
/>}
|
||||
{isGateway && hasSubscription && <ActivityJournalPanel
|
||||
feature={activityJournalFeature}
|
||||
loadPage={actions.loadActivityJournal}
|
||||
/>}
|
||||
<RoutingDiscardDialog feature={routingFeature} />
|
||||
<SubscriptionDeleteDialog feature={subscriptionFeature} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user