From fc3d588cecf4ad88d15126c665a8cc48cac951c1 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 30 Sep 2026 16:10:21 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=B4=D1=91=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B8=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D0=B8=D1=8F=20GRE-=D0=BF=D1=83=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/gre/page.tsx | 96 +++---------------- components/data-grids/gre-pools-data-grid.tsx | 49 +--------- docs/ux-ui-progress.md | 9 +- docs/ux-ui-stage-9.md | 8 ++ tests/e2e/gre-pools.spec.ts | 21 ++++ tests/e2e/ipsec-gre-tables.spec.ts | 11 +-- 6 files changed, 55 insertions(+), 139 deletions(-) create mode 100644 tests/e2e/gre-pools.spec.ts diff --git a/app/(main)/gre/page.tsx b/app/(main)/gre/page.tsx index 96238fb0..9c63a19a 100644 --- a/app/(main)/gre/page.tsx +++ b/app/(main)/gre/page.tsx @@ -11,7 +11,7 @@ import { GreTunnelsDataGrid } from "@/components/data-grids/gre-tunnels-data-gri import { GrePoolsDataGrid } from "@/components/data-grids/gre-pools-data-grid" import { FormField, FormToggle, SectionTitle } from "@/components/form-kit" import { greTunnels as mockGreTunnels, grePools as mockGrePools, servers as mockServers } from "@/lib/data" -import type { GrePool, GreTunnel, GreStatus, Server } from "@/lib/data" +import type { GreTunnel, GreStatus, Server } from "@/lib/data" import { useDataSource } from "@/lib/data-source" import { generateGreRsc } from "@/lib/gre-export" import { greResponseSchema } from "@/lib/gre-response" @@ -96,22 +96,6 @@ function mapBackendToServer(s: BackendServer): InventoryServer { } } -function derivePoolsFromTunnels(tunnels: GreTunnel[]): GrePool[] { - const ids = [...new Set(tunnels.map((t) => t.poolId).filter(Boolean))] - return ids.map((id) => { - const list = tunnels.filter((t) => t.poolId === id) - const allocated = list.length - return { - id, - name: id === "live" ? "С устройств (опрос)" : id, - cidr: "—", - allocated, - total: Math.max(allocated, 1), - comment: "По poolId из данных GRE", - } - }) -} - // ─── default form states ───────────────────────────────────────────────────── const defaultTunnelForm = { @@ -123,7 +107,6 @@ const defaultTunnelForm = { showAdvanced: false, } -const defaultPoolForm = { name: "", cidr: "", comment: "" } type TabFilter = "all" | "up" | "ipsec" | "plain" type PageTab = "tunnels" | "pools" @@ -180,12 +163,10 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on const [historyError, setHistoryError] = useState(null) const historyEpoch = useRef(0) const [historyRestoring, setHistoryRestoring] = useState(false) - const [poolOpen, setPoolOpen] = useState(false) const [codePreviewTunnel, setCodePreviewTunnel] = useState(null) const [exportContext, setExportContext] = useState<{ servers: Record; at: string | null; stale: boolean; mock: boolean } | null>(null) const [tForm, setTForm] = useState(defaultTunnelForm) - const [pForm, setPForm] = useState(defaultPoolForm) const setT = (k: K, v: (typeof defaultTunnelForm)[K]) => setTForm((f) => ({ ...f, [k]: v })) @@ -240,8 +221,8 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on const displayTunnels = isLive ? liveTunnels : mockGreTunnels const displayServers = isLive ? liveServers : mockServers const displayPools = useMemo( - () => (isLive ? derivePoolsFromTunnels(displayTunnels) : mockGrePools), - [isLive, displayTunnels], + () => (isLive ? [] : mockGrePools), + [isLive], ) const greRailItems = useMemo(() => ( @@ -670,8 +651,8 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on { id: "pools", label: "IP-пулов", - value: incomplete ? "—" : displayPools.length, - hint: incomplete ? "неизвестно" : undefined, + value: isLive ? "—" : displayPools.length, + hint: isLive ? "нет данных API" : "моковые данные", icon: , iconClassName: "text-primary", }, @@ -687,9 +668,9 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on {toggleError &&

{toggleError}

} {/* Page tabs */} -
+
{(["tunnels", "pools"] as PageTab[]).map((tab) => ( - @@ -735,13 +716,10 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on {/* ── IP Pools ── */} {pageTab === "pools" && ( - - {displayPools.length} пула - - + {isLive ? ( +
API GRE возвращает интерфейсы и назначенные IP. Каталог пулов, CIDR и свободная ёмкость не предоставляются; управление пулами здесь недоступно.
+ ) : <> + Моковый каталог · только просмотр
@@ -767,6 +745,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on })}
+ } )} @@ -884,13 +863,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
Внутренний IP - - - + {!isLive &&

Моковые пулы доступны только для просмотра.

}
setT("localInnerIp", e.target.value)} /> @@ -984,49 +957,6 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on - {/* ══ Sheet: Add Pool ════════════════════════════════════════════════════ */} - - - - Новый IP-пул - Пул адресов для назначения внутренних IP GRE-туннелям - -
-
- Параметры пула - - setPForm((f) => ({ ...f, name: e.target.value }))} /> - - - setPForm((f) => ({ ...f, cidr: e.target.value }))} /> - - - setPForm((f) => ({ ...f, comment: e.target.value }))} /> - - {pForm.cidr && /\/\d+$/.test(pForm.cidr) && (() => { - const prefix = parseInt(pForm.cidr.split("/")[1] ?? "0") - const blocks = prefix <= 30 ? Math.pow(2, 30 - prefix) : 0 - return blocks > 0 ? ( -
-

- Доступно {blocks} блоков /30 - {" "}= до {blocks} туннелей -

-
- ) : null - })()} -
-
- - }>Отмена - - -
-
- { if (!v) historyEpoch.current += 1; setHistoryOpen(v) }} diff --git a/components/data-grids/gre-pools-data-grid.tsx b/components/data-grids/gre-pools-data-grid.tsx index bee98d7b..8b2395f1 100644 --- a/components/data-grids/gre-pools-data-grid.tsx +++ b/components/data-grids/gre-pools-data-grid.tsx @@ -9,23 +9,14 @@ import { } from "@tanstack/react-table" import type { GrePool } from "@/lib/data" import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" import { DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, - DATA_GRID_CELL_PAD_LAST, } from "@/components/data-grids/shared/data-grid-layout" import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { EmptyState } from "@/components/empty-state" -import { LayersIcon, MoreHorizontalIcon, PencilIcon, Trash2Icon } from "lucide-react" +import { LayersIcon } from "lucide-react" interface GrePoolsDataGridProps { pools: GrePool[] @@ -130,44 +121,6 @@ function GrePoolsDataGrid({ pools }: GrePoolsDataGridProps) { cellClassName: DATA_GRID_CELL_PAD, }, }, - { - id: "actions", - header: () => Действия, - enableSorting: false, - cell: () => ( -
- - - - - } - /> - - - Редактировать - - - - Удалить пул - - - -
- ), - size: 56, - meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST }, - }, ], [], ) diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 3cbbd2d0..7ece17d4 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -25,12 +25,19 @@ | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | -| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.61 — IPsec/GRE/VXLAN, Firewall и операции Filters | +| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.62 — IPsec/GRE/VXLAN, Firewall и операции Filters | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 9.62 — возможности API GRE-пулов (2026-09-30) + +- Удалён фиктивный live-каталог из poolId интерфейсов: он не содержит CIDR/ёмкость, но раньше изображал полное заполнение и свободные блоки. Live KPI пулов теперь неизвестен с подписью «нет данных API», раздел объясняет отсутствие каталога/управления. Инвентаризация настоящих /ip/pool и её API не изобретены. +- Убраны кнопки создания пула, пустые меню изменения/удаления и selector пула, который не входил в GRE write-body. Mock-каталог остаётся только для просмотра с явной подписью; настройки локального IP остаются доступными. Кнопки разделов получили aria-pressed и имя группы. +- Семь E2E прошли: числовая сортировка/поиск 12 туннелей, отсутствие подмены неизвестных пулов пустыми, mock-каталог/no writes и три формы. TypeScript/lint чистые. +- Далее — desktop/mobile GRE/VXLAN и профильная регрессия. Расширение backend настоящим управлением адресными пулами — отдельный функциональный объём, текущий UX честно отражает доступные API. Локальный коммит без push. + ## Блок 9.61 — поддерживаемые параметры GRE/PSK и .rsc (2026-09-30) - GRE .rsc вынесен в generateGreRsc: quoted RouterOS strings/PSK, нормализованные комментарии, локальный IP только при наличии, без придуманных удалённых IP/маршрутов и дополнительных manual IPsec peer/policy/proposal поверх автоматического ipsec-secret. Параметры сверены с https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE и правилами Scripting. FastPath в снимке не меняется автоматически; несовместимый IPsec/FastPath подписан в комментарии. diff --git a/docs/ux-ui-stage-9.md b/docs/ux-ui-stage-9.md index acbd5790..88c14c02 100644 --- a/docs/ux-ui-stage-9.md +++ b/docs/ux-ui-stage-9.md @@ -353,3 +353,11 @@ Live-копирование читает актуальный целевой rul - Unit экспорта проверил строки/инъекцию новой команды/optional IP/no duplicate IPsec; шесть E2E PSK/create/edit/export прошли. TypeScript/lint чистые. RouterOS применение и реальное снятие PSK остаются внешней приёмкой. - Далее — достоверное отображение live-пулов и возможности API, затем layout и профильная регрессия GRE/VXLAN. Локальный коммит без push. + +## Блок 9.62 — возможности API GRE-пулов (2026-09-30) + +- Удалён фиктивный live-каталог из poolId интерфейсов: он не содержит CIDR/ёмкость, но раньше изображал полное заполнение и свободные блоки. Live KPI пулов теперь неизвестен с подписью «нет данных API», раздел объясняет отсутствие каталога/управления. Инвентаризация настоящих /ip/pool и её API не изобретены. +- Убраны кнопки создания пула, пустые меню изменения/удаления и selector пула, который не входил в GRE write-body. Mock-каталог остаётся только для просмотра с явной подписью; настройки локального IP остаются доступными. Кнопки разделов получили aria-pressed и имя группы. +- Семь E2E прошли: числовая сортировка/поиск 12 туннелей, отсутствие подмены неизвестных пулов пустыми, mock-каталог/no writes и три формы. TypeScript/lint чистые. +- Далее — desktop/mobile GRE/VXLAN и профильная регрессия. Расширение backend настоящим управлением адресными пулами — отдельный функциональный объём, текущий UX честно отражает доступные API. Локальный коммит без push. + diff --git a/tests/e2e/gre-pools.spec.ts b/tests/e2e/gre-pools.spec.ts new file mode 100644 index 00000000..00eb462b --- /dev/null +++ b/tests/e2e/gre-pools.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "@playwright/test" + +test("GRE: mock-пулы только для просмотра без фиктивного создания", async ({ page }) => { + let writes = 0 + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + if (r.request().method() !== "GET") writes++ + return r.fulfill({ json: new URL(r.request().url()).pathname === "/api/auth/config" ? { required: false } : {} }) + }) + await page.goto("/settings") + await page.getByRole("button", { name: "Моковые", exact: true }).click() + await page.getByRole("link", { name: /^GRE/ }).click() + await expect(page.getByRole("group", { name: "Свежесть GRE" })).toContainText("не получены с RouterOS") + await page.getByRole("button", { name: "IP-пулы", exact: true }).click() + await expect(page.getByText("Моковый каталог · только просмотр", { exact: true })).toBeVisible() + expect(await page.locator("tr[data-row-id]").count()).toBeGreaterThan(0) + await expect(page.getByRole("button", { name: "Добавить пул", exact: true })).toHaveCount(0) + await expect(page.getByRole("button", { name: "Создать пул", exact: true })).toHaveCount(0) + await expect(page.getByRole("menuitem", { name: "Удалить пул", exact: true })).toHaveCount(0) + expect(writes).toBe(0) +}) diff --git a/tests/e2e/ipsec-gre-tables.spec.ts b/tests/e2e/ipsec-gre-tables.spec.ts index bc9ac0fe..3cceb2f5 100644 --- a/tests/e2e/ipsec-gre-tables.spec.ts +++ b/tests/e2e/ipsec-gre-tables.spec.ts @@ -40,7 +40,7 @@ test("IPsec: 12 клиентов, сортировка, выбор удален await expect(rows).toHaveCount(1) }) -test("GRE: 12 туннелей и пулов, числовая сортировка и поиск", async ({ page }) => { +test("GRE: 12 туннелей, числовая сортировка и достоверность пулов", async ({ page }) => { const errors: string[] = [] page.on("pageerror", e => errors.push(e.message)) await setup(page, "/gre") @@ -52,11 +52,8 @@ test("GRE: 12 туннелей и пулов, числовая сортиров await expect(rows).toHaveCount(1) await page.getByPlaceholder("Поиск по имени, IP…").fill("") await page.getByRole("button", { name: "IP-пулы", exact: true }).click() - await expect(rows).toHaveCount(12) - await page.getByRole("button", { name: "Имя пула", exact: true }).click() - await expect(rows.first()).toContainText("pool-1") - await page.getByRole("button", { name: "Имя пула", exact: true }).click() - await expect(rows.first()).toContainText("pool-12") + await expect(rows).toHaveCount(0) + await expect(page.getByRole("status").filter({ hasText: "Каталог пулов, CIDR и свободная ёмкость не предоставляются" })).toBeVisible() expect(errors).toEqual([]) }) @@ -66,5 +63,5 @@ test("Пустые ответы IPsec и GRE", async ({ page }) => { await page.goto("/gre") await expect(page.getByText("Нет GRE-туннелей", { exact: true })).toBeVisible() await page.getByRole("button", { name: "IP-пулы", exact: true }).click() - await expect(page.getByText("Нет IP-пулов", { exact: true })).toBeVisible() + await expect(page.getByRole("status").filter({ hasText: "Каталог пулов, CIDR и свободная ёмкость не предоставляются" })).toBeVisible() })