From f0071a495c5a5e54a8653efd03cdf4bc6dcb979a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 30 Sep 2026 14:48:21 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=83=D1=87=D0=B8=D1=82=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20=D0=B2=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D1=8F=D1=85=20IPsec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/ipsec/page.tsx | 25 +++++++++++--- components/ipsec/ipsec-server-grid.tsx | 12 ++++--- components/ipsec/ipsec-users-grid.tsx | 7 +++- docs/ux-ui-progress.md | 9 ++++- docs/ux-ui-stage-9.md | 7 ++++ tests/e2e/ipsec-gre-tables.spec.ts | 2 +- tests/e2e/ipsec-readiness.spec.ts | 47 ++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/ipsec-readiness.spec.ts diff --git a/app/(main)/ipsec/page.tsx b/app/(main)/ipsec/page.tsx index f241af4c..87ca624f 100644 --- a/app/(main)/ipsec/page.tsx +++ b/app/(main)/ipsec/page.tsx @@ -300,6 +300,13 @@ function IpsecPage() { const failedServerIds = new Set(failures.map(f => f.serverId)) const scopeUnknown = isLive && (!hasLoaded || !!dataError || (effectiveServerId === ALL_SERVERS_ID ? failures.length > 0 : failedServerIds.has(effectiveServerId))) + const canManage = (serverId: string) => isLive && hasLoaded && !dataError && !inventoryError && + !loading && !busy && !historyRestoring && !failedServerIds.has(serverId) && liveServers.some(s => s.id === serverId) + const assertDeviceReady = (serverId: string) => { + if (!canManage(serverId)) throw new Error("Сначала подтвердите актуальное состояние устройства: повторите опрос IPsec") + } + const hasReadyDevice = displayServers.some(s => canManage(s.id)) + const scopedClients = useMemo(() => { if (effectiveServerId === ALL_SERVERS_ID) return displayClients return displayClients.filter((c) => c.serverId === effectiveServerId) @@ -370,6 +377,7 @@ function IpsecPage() { }, [displayServers, displayClients]) const handleInit = async (form: IpsecInitFormState) => { + assertDeviceReady(form.serverId) setBusy(true) try { await initIpsecServer(backendUrl, { @@ -391,6 +399,7 @@ function IpsecPage() { } const handleCreateUser = async (form: IpsecUserFormState) => { + assertDeviceReady(form.serverId) setBusy(true) try { const res = await createIpsecUser(backendUrl, { @@ -424,6 +433,7 @@ function IpsecPage() { const handleEditUser = async (form: IpsecUserFormState) => { if (!editing) return + assertDeviceReady(editing.serverId) setBusy(true) try { await patchIpsecUser(backendUrl, editing.serverId, editing.rosId, { @@ -490,6 +500,8 @@ function IpsecPage() { const confirmDelete = async (force = false) => { if (!pendingDelete || deleteLock.current || (force && !forceAllowed)) return const target = pendingDelete + const serverId = target.kind === "user" ? target.client.serverId : target.kind === "server" ? target.summary.serverId : target.serverId + if (!canManage(serverId)) { setDeleteError("Повторите опрос IPsec перед удалением"); return } deleteLock.current = true setBusy(true) setDeleteError(null) @@ -523,6 +535,7 @@ function IpsecPage() { const handleEditPeer = async (form: IpsecPeerFormState) => { if (!editingPeer) return + assertDeviceReady(editingPeer.serverId) setBusy(true) try { await patchIpsecPeer(backendUrl, editingPeer.serverId, editingPeer.peer.rosId, { @@ -586,7 +599,7 @@ function IpsecPage() { )} {isLive ? ( - @@ -713,7 +726,7 @@ function IpsecPage() { searchPlaceholder="Поиск по имени / CN / IP…" actions={ isLive ? ( - @@ -722,6 +735,7 @@ function IpsecPage() { /> {isLive && !hasLoaded ?

{loading ? "Загрузка клиентов IPsec…" : "Клиенты IPsec пока неизвестны"}

: {isLive && !hasLoaded ?

Состояние серверов IPsec пока неизвестно

: { setSelectedServerId(s.serverId); setInitOpen(true) } : undefined} onRemove={isLive ? (s) => setPendingDelete({ kind: "server", summary: s }) : undefined} onEditPeer={isLive ? openEditPeer : undefined} @@ -814,7 +829,7 @@ function IpsecPage() { { setUserOpen(v); if (!v) setEditing(null) }} - servers={serverOptions} + servers={serverOptions.filter(s => canManage(s.id) || busy)} peers={peerOptions} busy={busy} defaultServerId={sheetServerId} diff --git a/components/ipsec/ipsec-server-grid.tsx b/components/ipsec/ipsec-server-grid.tsx index 8742bbf4..44cf1fc5 100644 --- a/components/ipsec/ipsec-server-grid.tsx +++ b/components/ipsec/ipsec-server-grid.tsx @@ -150,6 +150,7 @@ function CertRow({ } export interface IpsecServerGridProps { + canManage?: (serverId: string) => boolean servers: IpsecServerSummaryDto[] onInit?: (row: IpsecServerSummaryDto) => void onRemove?: (row: IpsecServerSummaryDto) => void @@ -161,6 +162,7 @@ export interface IpsecServerGridProps { function IpsecServerGrid({ servers, + canManage, onInit, onRemove, onEditPeer, @@ -208,8 +210,8 @@ function IpsecServerGrid({ onEditPeer(s.serverId, peer) : undefined} - onDelete={onDeletePeer ? (peer) => onDeletePeer(s.serverId, peer) : undefined} + onEdit={onEditPeer && (!canManage || canManage(s.serverId)) ? (peer) => onEditPeer(s.serverId, peer) : undefined} + onDelete={onDeletePeer && (!canManage || canManage(s.serverId)) ? (peer) => onDeletePeer(s.serverId, peer) : undefined} /> ))} @@ -228,8 +230,8 @@ function IpsecServerGrid({ onDeleteCert(s.serverId, cert) : undefined} - onExport={onExportCert ? (cert) => onExportCert(s.serverId, cert) : undefined} + onDelete={onDeleteCert && (!canManage || canManage(s.serverId)) ? (cert) => onDeleteCert(s.serverId, cert) : undefined} + onExport={onExportCert && (!canManage || canManage(s.serverId)) ? (cert) => onExportCert(s.serverId, cert) : undefined} /> ))} @@ -250,6 +252,7 @@ function IpsecServerGrid({ type="button" size="sm" variant="outline" + disabled={!onInit || (canManage && !canManage(s.serverId))} onClick={() => onInit?.(s)} title={s.initialized ? "Обновить managed-конфиг сервера" : "Инициализировать IKEv2-сервер"} > @@ -264,6 +267,7 @@ function IpsecServerGrid({ className="size-8 text-destructive" aria-label="Удалить IKEv2-сервер" title="Удалить managed-объекты IKEv2 (identity/mode-config/peer/pool/NAT + сертификаты)" + disabled={canManage && !canManage(s.serverId)} onClick={() => onRemove(s)} > diff --git a/components/ipsec/ipsec-users-grid.tsx b/components/ipsec/ipsec-users-grid.tsx index ab53f730..c5ac39e7 100644 --- a/components/ipsec/ipsec-users-grid.tsx +++ b/components/ipsec/ipsec-users-grid.tsx @@ -28,6 +28,7 @@ import { } from "lucide-react" export interface IpsecUsersGridProps { + canManage?: (serverId: string) => boolean clients: IpsecClientDto[] compactServer?: boolean emptyAction?: ReactNode @@ -40,6 +41,7 @@ export interface IpsecUsersGridProps { function IpsecUsersGrid({ clients, + canManage, compactServer = false, emptyAction, emptyTitle = "Нет клиентов IKEv2", @@ -63,6 +65,7 @@ function IpsecUsersGrid({