From 583b23ded132995a2c20100fa9e0f90373e7f578 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 14 Sep 2026 08:31:16 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B?= =?UTF-8?q?=20IPsec=20=D0=B8=20GRE=20=D0=BD=D0=B0=20ReUI=20v9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/data-grids/gre-pools-data-grid.tsx | 22 +++--- .../data-grids/gre-tunnels-data-grid.tsx | 22 +++--- components/ipsec/ipsec-users-grid.tsx | 24 +++---- docs/ux-ui-progress.md | 12 +++- tests/e2e/ipsec-gre-tables.spec.ts | 70 +++++++++++++++++++ 5 files changed, 115 insertions(+), 35 deletions(-) create mode 100644 tests/e2e/ipsec-gre-tables.spec.ts diff --git a/components/data-grids/gre-pools-data-grid.tsx b/components/data-grids/gre-pools-data-grid.tsx index 90922ffc..bee98d7b 100644 --- a/components/data-grids/gre-pools-data-grid.tsx +++ b/components/data-grids/gre-pools-data-grid.tsx @@ -1,12 +1,12 @@ "use client" +import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" + import { useMemo } from "react" import { type ColumnDef, - getCoreRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table-v8" + useTable, +} from "@tanstack/react-table" import type { GrePool } from "@/lib/data" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" @@ -17,13 +17,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell" +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/legacy-data-grid-layout" -import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header" +} 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" @@ -32,7 +32,7 @@ interface GrePoolsDataGridProps { } function GrePoolsDataGrid({ pools }: GrePoolsDataGridProps) { - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { id: "name", @@ -172,11 +172,11 @@ function GrePoolsDataGrid({ pools }: GrePoolsDataGridProps) { [], ) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: true, data: pools, columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.id, }) diff --git a/components/data-grids/gre-tunnels-data-grid.tsx b/components/data-grids/gre-tunnels-data-grid.tsx index 3ea3101c..4eb4e85d 100644 --- a/components/data-grids/gre-tunnels-data-grid.tsx +++ b/components/data-grids/gre-tunnels-data-grid.tsx @@ -1,12 +1,12 @@ "use client" +import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" + import { useMemo } from "react" import { type ColumnDef, - getCoreRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table-v8" + useTable, +} from "@tanstack/react-table" import type { GrePool, GreTunnel, @@ -29,13 +29,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell" +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/legacy-data-grid-layout" -import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header" +} 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 { CodeXmlIcon, @@ -120,7 +120,7 @@ function GreTunnelsDataGrid({ const serverMap = useMemo(() => new Map(servers.map((s) => [s.id, s])), [servers]) const poolMap = useMemo(() => new Map(pools.map((p) => [p.id, p])), [pools]) - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { id: "name", @@ -340,11 +340,11 @@ function GreTunnelsDataGrid({ [onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, poolMap, serverMap], ) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: true, data: tunnels, columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), getRowId: (row, index) => `${row.id}:${row.serverId}:${row.name}:${index}`, }) diff --git a/components/ipsec/ipsec-users-grid.tsx b/components/ipsec/ipsec-users-grid.tsx index 4ac48b50..f20a53e5 100644 --- a/components/ipsec/ipsec-users-grid.tsx +++ b/components/ipsec/ipsec-users-grid.tsx @@ -1,23 +1,23 @@ "use client" +import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" + import { useMemo, type ReactNode } from "react" import { type ColumnDef, - getCoreRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table-v8" + useTable, +} from "@tanstack/react-table" import type { IpsecClientDto } from "@mmapp/contracts/ipsec" import { cn } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell" +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/legacy-data-grid-layout" -import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header" +} 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 { BadgeCheckIcon, @@ -44,8 +44,8 @@ function IpsecUsersGrid({ onEdit, onDelete, }: IpsecUsersGridProps) { - const columns = useMemo[]>(() => { - const cols: ColumnDef[] = [ + const columns = useMemo[]>(() => { + const cols: ColumnDef[] = [ { id: "name", accessorKey: "name", @@ -224,11 +224,11 @@ function IpsecUsersGrid({ return cols }, [compactServer, onDownloadCert, onEdit, onDelete]) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: true, data: clients, columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.id, }) diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 7b332b4b..1e9295d0 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -18,7 +18,7 @@ | 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 | | 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки | | 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже | -| 4 | Остальные таблицы, удаление старых API, общая регрессия | В работе: BGP/OSPF, WireGuard и VXLAN перенесены | +| 4 | Остальные таблицы, удаление старых API, общая регрессия | В работе: BGP/OSPF и VPN-группа перенесены | | 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат | | 6 | Навигация и дашборд | Не начат | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат | @@ -236,6 +236,16 @@ TypeScript (`--incremental false`), целевой ESLint и браузерны После проверок осталось 8% пятичасового лимита (общая квота аккаунта). Блок сохранить локальным коммитом без push. Следующий шаг после сброса: `ipsec-users-grid`, `gre-pools-data-grid`, `gre-tunnels-data-grid`; затем оставшиеся таблицы управления/диагностики и старые Filters. VXLAN исключить из предыдущего списка остатка. Этап 4 не завершён, ветку не выпускать. +## Этап 4 — блок 4: IPsec и GRE (2026-09-14) + +Перенесены `ipsec-users-grid`, `gre-tunnels-data-grid`, `gre-pools-data-grid`: `useTable`, `dataGridFeatures`, типы и общая оболочка ReUI v9. Явное `manualPagination: true` сохраняет полный список. Сохранены существующие идентификаторы и обработчики; ID GRE по-прежнему включает индекс исходных данных, его стабильность при переупорядочивании ответа API не улучшалась в этом блоке. Установленные примитивы и сетевые правила не менялись. + +Проверки: TypeScript (`--incremental false`), целевой ESLint и три браузерных сценария `tests/e2e/ipsec-gre-tables.spec.ts` прошли. Проверены 12 клиентов IPsec, сортировка и подтверждение удаления выбранного клиента с отменой; 12 GRE-туннелей и производных пулов, числовая сортировка MTU, поиск, сортировка пулов и пустые ответы API. Селектор заголовка «Клиент» уточнён после совпадения с кнопкой панели. IPsec-фикстура валидируется контрактной схемой; GRE типизирован через доменную модель. Используются HTTP-моки: применение RouterOS, удаление клиента и экспорт сертификата этим прогоном не проверены. + +ReUI checklist получен; сохранены публичный API, композиция и оформление. Полная доступность, адаптивность, работа операций и существующие декоративные действия GRE-пулов остаются профильному этапу 9 и итоговой приёмке. Общая основа не менялась; полная сборка и регрессия остаются завершению этапа 4. + +Квота: в начале использовано 1%, после проверок 18% пятичасового окна (общий расход аккаунта). VPN-группа миграции завершена, этап 4 в целом ещё открыт. Следующий конкретный блок — `users-data-grid`, `subusers-data-grid`, `certificates-data-grid`, `backups-data-grid`; затем оставшиеся таблицы диагностики/управления и Filters. Только после последнего потребителя удалить legacy-основу и v8 и выполнить общую регрессию. Блок IPsec/GRE сохранить локальным коммитом без push; ветку до завершения этапа 4 не выпускать. + ## Карта миграции (исходная инвентаризация этапа 0) Текущая связка: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. Актуальный ReUI MCP ранее подтвердил useTable/dataGridFeatures для Data Grid и дерево FilterQuery вместо filters/onChange для Filters. Перед установкой перепроверить совместимые версии, сохранить семантику существующих фильтров (простые условия → AND) и закрепить lockfile. diff --git a/tests/e2e/ipsec-gre-tables.spec.ts b/tests/e2e/ipsec-gre-tables.spec.ts new file mode 100644 index 00000000..e146c7d4 --- /dev/null +++ b/tests/e2e/ipsec-gre-tables.spec.ts @@ -0,0 +1,70 @@ +import { test, expect, type Page } from "@playwright/test" +import { ipsecListResponseSchema } from "../../packages/contracts/src/ipsec" +import type { GreTunnel } from "../../lib/data" + +const ids = Array.from({ length: 12 }, (_, i) => i + 1) +const ipsec = ipsecListResponseSchema.parse({ servers: [], clients: ids.map(i => ({ + id: String(i), rosId: `*${i}`, serverId: "1", serverName: "Тестовый роутер", + name: `Клиент ${i}`, authMethod: "certificate", online: false, disabled: false, managed: true, +})) }) +const tunnels: GreTunnel[] = ids.map(i => ({ + id: String(i), name: `gre-test-${i}`, serverId: "1", localAddress: "192.0.2.1", + remoteAddress: `192.0.2.${i + 1}`, localInnerIp: `10.0.${i}.1/30`, remoteInnerIp: `10.0.${i}.2/30`, + poolId: `pool-${i}`, ipsec: null, mtu: 1400 + i, keepaliveInterval: 10, keepaliveRetries: 3, + dscp: "inherit", clampTcpMss: true, allowFastPath: true, comment: "Тест", enabled: true, status: "up", +})) + +async function setup(page: Page, path: string, empty = false) { + await page.route("**/health", route => route.fulfill({ json: { ok: true } })) + await page.route("**/api/**", route => { + const path = new URL(route.request().url()).pathname + return route.fulfill({ json: path === "/api/ipsec" ? (empty ? { servers: [], clients: [] } : ipsec) + : path === "/api/gre/tunnels" ? { tunnels: empty ? [] : tunnels } + : path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [] : {} }) + }) + await page.goto(path) +} + +test("IPsec: 12 клиентов, сортировка, выбор удаления и поиск", async ({ page }) => { + await setup(page, "/ipsec") + const rows = page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(12) + await page.getByRole("table").getByRole("button", { name: "Клиент", exact: true }).click() + await expect(rows.first()).toContainText("Клиент 1") + await page.getByRole("table").getByRole("button", { name: "Клиент", exact: true }).click() + await expect(rows.first()).toContainText("Клиент 12") + await rows.first().getByRole("button", { name: "Удалить клиента" }).click() + await expect(page.getByRole("alertdialog")).toContainText("Клиент 12") + await page.getByRole("button", { name: "Отмена", exact: true }).click() + await page.getByPlaceholder("Поиск по имени / CN / IP…").fill("Клиент 12") + await expect(rows).toHaveCount(1) +}) + +test("GRE: 12 туннелей и пулов, числовая сортировка и поиск", async ({ page }) => { + const errors: string[] = [] + page.on("pageerror", e => errors.push(e.message)) + await setup(page, "/gre") + const rows = page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(12) + await page.getByRole("button", { name: "MTU", exact: true }).click() + await expect(rows.first()).toContainText("gre-test-12") + await page.getByPlaceholder("Поиск по имени, IP…").fill("gre-test-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") + expect(errors).toEqual([]) +}) + +test("Пустые ответы IPsec и GRE", async ({ page }) => { + await setup(page, "/ipsec", true) + await expect(page.getByText("Нет клиентов IKEv2", { exact: true })).toBeVisible() + 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() +})