From 5cad3e90617bf631efb229be621d83fa518a297f Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Tue, 11 Aug 2026 07:36:44 +0300 Subject: [PATCH] Simplify subscription profile management UI --- src/shared/versions.ts | 4 +- src/web/App.tsx | 7 - src/web/components/ClientOverviewPage.tsx | 10 +- src/web/features/servers/ServerPicker.tsx | 4 +- .../subscription/SubscriptionFeature.tsx | 150 +++++------------- src/web/styles/features/servers.css | 19 ++- src/web/styles/features/subscription.css | 141 +++++++--------- test/web/device-inventory-contract.test.js | 4 +- test/web/responsive-layout-contract.test.js | 7 +- test/web/server-picker.test.js | 10 +- test/web/style-boundaries.test.js | 30 ++-- .../web/subscription-feature-contract.test.js | 13 +- 12 files changed, 148 insertions(+), 251 deletions(-) diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 9d10c7b..b3b8bcf 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.23.2', - gatewayClient: '0.24.2', + macClient: '0.23.3', + gatewayClient: '0.24.3', gatewayBackend: '0.24.0', }); diff --git a/src/web/App.tsx b/src/web/App.tsx index 0a875af..89d9b2c 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -248,13 +248,6 @@ export function App() { 'subscription', label, )} - onRenameProfile={(profileId: string, label: string) => run( - 'profileRename', - () => api.profiles.rename(profileId, label, revisionRef.current), - 'subscription', - profileId, - profileId, - )} onSelectProfileServer={(profileId: string, serverId: string) => run( 'profileSelect', () => api.profiles.selectServer(profileId, serverId, revisionRef.current), diff --git a/src/web/components/ClientOverviewPage.tsx b/src/web/components/ClientOverviewPage.tsx index e81fac1..742de10 100644 --- a/src/web/components/ClientOverviewPage.tsx +++ b/src/web/components/ClientOverviewPage.tsx @@ -8,6 +8,7 @@ import React, { import { copyText, localProxyUrls, + subscriptionDomain, } from '../utils/clientControls.js'; import { operationBlocked, @@ -103,7 +104,6 @@ interface ClientOverviewPageProps { operations?: OperationRegistrySnapshot; error: UiError | null; onAddProfile: (label: string, url: string) => Promise; - onRenameProfile: (profileId: string, label: string) => Promise; onSelectProfileServer: (profileId: string, serverId: string) => Promise; onActivateProfile: (profileId: string) => Promise; onRefreshProfile: (profileId: string) => Promise; @@ -410,7 +410,6 @@ export function ClientOverviewPage({ operations = {}, error, onAddProfile, - onRenameProfile, onSelectProfileServer, onActivateProfile, onRefreshProfile, @@ -493,7 +492,6 @@ export function ClientOverviewPage({ isGateway, gatewayDirect, onAdd: onAddProfile, - onRename: onRenameProfile, onRefresh: onRefreshProfile, onForget: onForgetProfile, onActivate: onActivateProfile, @@ -608,15 +606,15 @@ export function ClientOverviewPage({ ? 'Gateway · сервер не определён' : connected ? appliedProfile && appliedServer - ? `${appliedProfile.label} · ${appliedServer.label}` + ? `${subscriptionDomain(appliedProfile.subscription.host)} · ${appliedServer.label}` : 'VPN · сервер не определён' : desiredProfile && desiredServer - ? `Выбран: ${desiredProfile.label} · ${desiredServer.label}` + ? `Выбран: ${subscriptionDomain(desiredProfile.subscription.host)} · ${desiredServer.label}` : 'Сервер не выбран'; const switchIdentity = gatewayDirect ? 'Данные применённого сервера Gateway недоступны' : switchingServer && operationProfile && operationServer - ? `Переключаем на ${operationProfile.label} · ${operationServer.label}` + ? `Переключаем на ${subscriptionDomain(operationProfile.subscription.host)} · ${operationServer.label}` : ''; return ( diff --git a/src/web/features/servers/ServerPicker.tsx b/src/web/features/servers/ServerPicker.tsx index 2b9f14f..257dc16 100644 --- a/src/web/features/servers/ServerPicker.tsx +++ b/src/web/features/servers/ServerPicker.tsx @@ -93,7 +93,7 @@ function ServerCheckButton({ onClick: () => void; }) { return ; } diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index 993bcd6..90fafc1 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -40,15 +40,12 @@ interface SubscriptionFeatureOptions { operations: OperationRegistrySnapshot; error?: ProfileError | null; onAdd: (label: string, url: string) => Promise; - onRename: (profileId: string, label: string) => Promise; onRefresh: (profileId: string) => Promise; onForget: (profileId: string, mode: 'delete' | 'stop-and-delete') => Promise; onActivate: (profileId: string) => Promise; onDismissError: () => void; } -const foldLabel = (value: string) => value.trim().toLocaleLowerCase('ru-RU'); - function operationText(key: OperationKey) { return ({ profileAdd: 'Добавляем подписку…', @@ -74,7 +71,6 @@ export function useSubscriptionFeature({ operations, error, onAdd, - onRename, onRefresh, onForget, onActivate, @@ -84,11 +80,7 @@ export function useSubscriptionFeature({ const [expanded, setExpanded] = useState([]); const [visited, setVisited] = useState([]); const [adding, setAdding] = useState(profiles.length === 0); - const [label, setLabel] = useState(''); const [url, setUrl] = useState(''); - const [renamingId, setRenamingId] = useState(''); - const [renameLabel, setRenameLabel] = useState(''); - const [menuId, setMenuId] = useState(''); const [deleteId, setDeleteId] = useState(''); const [refreshingIds, setRefreshingIds] = useState([]); const [revealVersions, setRevealVersions] = useState>({}); @@ -101,17 +93,10 @@ export function useSubscriptionFeature({ deleteIdRef.current = deleteId; const normalizedUrl = url.trim(); - const normalizedLabel = label.trim(); - const duplicateLabel = Boolean(normalizedLabel && profiles.some( - (profile) => foldLabel(profile.label) === foldLabel(normalizedLabel), - )); const addError = !error?.profileId && error?.context === 'subscription' ? error : null; const validationStatus = !normalizedUrl ? 'idle' : isSubscriptionUrlValid(normalizedUrl) ? 'valid' : 'invalid'; - const renameDuplicate = Boolean(renameLabel.trim() && profiles.some((profile) => ( - profile.id !== renamingId && foldLabel(profile.label) === foldLabel(renameLabel) - ))); const deleteProfile = profiles.find((profile) => profile.id === deleteId) || null; const deleteStopsVpn = connected && selection.appliedProfileId === deleteId; const activeOperation = (Object.entries(operations) as Array<[ @@ -154,7 +139,6 @@ export function useSubscriptionFeature({ panelRef.current?.contains(target) || toggleRef.current?.contains(target) )) return; setOpen(false); - setMenuId(''); }; document.addEventListener('pointerdown', closeDrawer); document.addEventListener('keydown', closeDrawer); @@ -169,7 +153,6 @@ export function useSubscriptionFeature({ }, [open]); function resetAdd() { - setLabel(''); setUrl(''); if (profiles.length) setAdding(false); if (profiles.length) requestAnimationFrame(() => ( @@ -178,31 +161,17 @@ export function useSubscriptionFeature({ } async function addProfile() { - if (!normalizedLabel || duplicateLabel || validationStatus !== 'valid') return; - if (!await onAdd(normalizedLabel, normalizedUrl)) return; + if (validationStatus !== 'valid') return; + const host = new URL(normalizedUrl).hostname; + const usedLabels = new Set(profiles.map((profile) => profile.label.toLocaleLowerCase('ru-RU'))); + let label = host; + for (let suffix = 2; usedLabels.has(label.toLocaleLowerCase('ru-RU')); suffix += 1) { + label = `${host} ${suffix}`; + } + if (!await onAdd(label, normalizedUrl)) return; resetAdd(); } - function startRename(profile: ProfileSnapshot) { - setMenuId(''); - setRenamingId(profile.id); - setRenameLabel(profile.label); - onDismissError(); - } - - function cancelRename() { - const profileId = renamingId; - setRenamingId(''); - requestAnimationFrame(() => document.getElementById(`client-profile-menu-${profileId}`)?.focus()); - } - - async function saveRename() { - const next = renameLabel.trim(); - if (!next || renameDuplicate) return; - if (!await onRename(renamingId, next)) return; - cancelRename(); - } - async function refresh(profileId: string) { const startedAt = performance.now(); setRefreshingIds((current) => current.includes(profileId) ? current : [...current, profileId]); @@ -225,7 +194,6 @@ export function useSubscriptionFeature({ if (!deleteProfile) return; if (!await onForget(deleteProfile.id, deleteStopsVpn ? 'stop-and-delete' : 'delete')) return; setDeleteId(''); - setMenuId(''); } function toggleProfile(profileId: string) { @@ -247,15 +215,9 @@ export function useSubscriptionFeature({ expanded, visited, adding, - label, url, validationStatus, - duplicateLabel, addError, - renamingId, - renameLabel, - renameDuplicate, - menuId, deleteProfile, deleteStopsVpn, activeOperation, @@ -266,30 +228,22 @@ export function useSubscriptionFeature({ closeRef, labelRef, addBlocked: operationBlocked(operations, 'profileAdd'), - renameBlocked: operationBlocked(operations, 'profileRename'), refreshBlocked: operationBlocked(operations, 'profileRefresh'), deleteBlocked: operationBlocked(operations, 'profileDelete'), activateBlocked: operationBlocked(operations, 'profileActivate'), toggle: () => setOpen((current) => !current), - close: () => { setOpen(false); setMenuId(''); }, + close: () => setOpen(false), showAdd: () => { setAdding(true); - setMenuId(''); onDismissError(); }, cancelAdd: resetAdd, - setLabel: (value: string) => { setLabel(value); onDismissError(); }, setUrl: (value: string) => { setUrl(value); onDismissError(); }, addProfile, toggleProfile, - startRename, - setRenameLabel, - cancelRename, - saveRename, - toggleMenu: (profileId: string) => setMenuId((current) => current === profileId ? '' : profileId), requestDelete: (profileId: string) => setDeleteId(profileId), cancelDelete: () => setDeleteId(''), confirmDelete, @@ -324,10 +278,8 @@ export function SubscriptionToggle({ } function AddProfileForm({ feature }: { feature: SubscriptionFeatureController }) { - const message = feature.duplicateLabel - ? 'Такое имя уже используется.' - : feature.validationStatus === 'invalid' - ? ERROR_DEFINITIONS.SUBSCRIPTION_INVALID.message + const message = feature.validationStatus === 'invalid' + ? ERROR_DEFINITIONS.SUBSCRIPTION_INVALID.message : feature.addError?.message || ''; return
-