fix(ui): перенести таблицы IPsec и GRE на ReUI v9
This commit is contained in:
1 parent
8b9b584513
commit
583b23ded1
5 files changed
+115
-35
No files matched your search
@@ -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<ColumnDef<GrePool>[]>(
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, GrePool>[]>(
|
||||
() => [
|
||||
{
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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<ColumnDef<GreTunnel>[]>(
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, GreTunnel>[]>(
|
||||
() => [
|
||||
{
|
||||
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}`,
|
||||
})
|
||||
|
||||
|
||||
@@ -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<ColumnDef<IpsecClientDto>[]>(() => {
|
||||
const cols: ColumnDef<IpsecClientDto>[] = [
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, IpsecClientDto>[]>(() => {
|
||||
const cols: ColumnDef<DataGridFeatures, IpsecClientDto>[] = [
|
||||
{
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
+11
-1
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
Reference in new issue
Block a user