fix(ui): перенести таблицы BGP и OSPF на ReUI v9
This commit is contained in:
1 parent
e66762ca2e
commit
d3db82fd1c
6 files changed
+132
-49
No files matched your search
@@ -1,20 +1,19 @@
|
||||
"use client"
|
||||
|
||||
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
getExpandedRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table-v8"
|
||||
useTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { cn } from "@/lib/utils"
|
||||
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,
|
||||
} 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 { BgpSessionDetail } from "@/components/data-grids/bgp-session-detail"
|
||||
import type { BgpSessionRow, BgpState, BgpType } from "@/lib/bgp/types"
|
||||
@@ -104,7 +103,7 @@ interface BgpSessionsDataGridProps {
|
||||
}
|
||||
|
||||
function BgpSessionsDataGrid({ sessions }: BgpSessionsDataGridProps) {
|
||||
const columns = useMemo<ColumnDef<BgpSessionRow>[]>(
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, BgpSessionRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "serverLabel",
|
||||
@@ -260,12 +259,11 @@ function BgpSessionsDataGrid({ sessions }: BgpSessionsDataGridProps) {
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
manualPagination: true,
|
||||
data: sessions,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
getRowId: (row) => row.id,
|
||||
getRowCanExpand: () => true,
|
||||
})
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"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 { cn } from "@/lib/utils"
|
||||
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 { ActivityIcon } from "lucide-react"
|
||||
|
||||
@@ -69,7 +69,7 @@ interface OspfBfdDataGridProps {
|
||||
}
|
||||
|
||||
function OspfBfdDataGrid({ sessions }: OspfBfdDataGridProps) {
|
||||
const columns = useMemo<ColumnDef<BfdSessionRow>[]>(
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, BfdSessionRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "serverLabel",
|
||||
@@ -267,11 +267,11 @@ function OspfBfdDataGrid({ sessions }: OspfBfdDataGridProps) {
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
manualPagination: true,
|
||||
data: sessions,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getRowId: (row) => row.id,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"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 { cn } from "@/lib/utils"
|
||||
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 { NetworkIcon } from "lucide-react"
|
||||
|
||||
@@ -53,10 +53,9 @@ function OspfNeighborsDataGrid({
|
||||
neighbors,
|
||||
highlightRouterId,
|
||||
selectedRouterId,
|
||||
onHighlight,
|
||||
onSelect,
|
||||
}: OspfNeighborsDataGridProps) {
|
||||
const columns = useMemo<ColumnDef<OspfNeighborRow>[]>(
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, OspfNeighborRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "localLabel",
|
||||
@@ -192,14 +191,14 @@ function OspfNeighborsDataGrid({
|
||||
const match = neighbors.find(
|
||||
(n) => n.localRouter === selectedRouterId || n.remoteRouter === selectedRouterId,
|
||||
)
|
||||
return match ? { [match.id]: true } : {}
|
||||
return match ? { [match.id]: true as const } : {}
|
||||
}, [neighbors, selectedRouterId])
|
||||
|
||||
const table = useReactTable({
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
manualPagination: true,
|
||||
data: neighbors,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getRowId: (row) => row.id,
|
||||
enableRowSelection: true,
|
||||
state: { rowSelection },
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"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 { cn } from "@/lib/utils"
|
||||
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 { RouteIcon } from "lucide-react"
|
||||
|
||||
@@ -42,7 +42,7 @@ interface OspfRoutesDataGridProps {
|
||||
}
|
||||
|
||||
function OspfRoutesDataGrid({ routes }: OspfRoutesDataGridProps) {
|
||||
const columns = useMemo<ColumnDef<OspfRouteRow>[]>(
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, OspfRouteRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "destination",
|
||||
@@ -140,11 +140,11 @@ function OspfRoutesDataGrid({ routes }: OspfRoutesDataGridProps) {
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
manualPagination: true,
|
||||
data: routes,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getRowId: (row) => row.id,
|
||||
})
|
||||
|
||||
|
||||
+13
-1
@@ -18,7 +18,7 @@
|
||||
| 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 |
|
||||
| 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки |
|
||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Не начат |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | В работе: блок BGP/OSPF перенесён |
|
||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат |
|
||||
| 6 | Навигация и дашборд | Не начат |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||
@@ -206,6 +206,18 @@ Production build прошёл: `npm run build`, 34 статические стр
|
||||
|
||||
Следующий шаг: проверить квоту и git status; начать этап 4 в `codex/reui-v9-migration`, переносить оставшихся потребителей ограниченными группами, затем удалить v8/старые Filters и выполнить общую регрессию. Ветку до этой проверки не выпускать. Коммит этапа 3 — только локальный, без push; настройки skills/AGENTS/.codex других работ не включать.
|
||||
|
||||
## Этап 4 — блок 1: BGP и OSPF
|
||||
|
||||
Перенесены `bgp-sessions-data-grid`, `ospf-neighbors-data-grid`, `ospf-routes-data-grid`, `ospf-bfd-data-grid`. Использована установленная основа ReUI v9 (`useTable`, `dataGridFeatures`, современная оболочка и заголовки); `manualPagination: true` сохраняет полный список. Стабильные row ID, сортировка, раскрытие BGP и управляемый выбор OSPF сохранены. Убрано неиспользуемое извлечение `onHighlight` из props, сам внешний контракт не менялся.
|
||||
|
||||
Проверки: `npx tsc --noEmit --incremental false` и целевой ESLint успешны. Три сценария `tests/e2e/routing-tables.spec.ts` подтвердили 12 строк во всех четырёх таблицах, числовую сортировку, выбор OSPF-соседа, раскрытие/сворачивание BGP после сортировки и пустые ответы API. Один тест сначала ожидал сокращённый текст пустого BFD; после уточнения селектора прошёл. Это HTTP-моки, не подтверждение реальной работы сетевых протоколов. Общая основа не менялась, повторная полная сборка/регрессия отложены до завершения этапа 4.
|
||||
|
||||
ReUI checklist получен: сохранены композиция, props установленного API, источники данных и оформление. Полная клавиатурная доступность действий строк, мобильная компоновка и сценарии графа остаются профильному этапу 10 и итоговой приёмке; текущие тесты их не подтверждают.
|
||||
|
||||
Квота: на старте использовано 49%, после блока 58% пятичасового окна (общий расход аккаунта). Этап 4 не завершён. BGP и три OSPF-таблицы исключить из списка оставшихся выше; остальные legacy-таблицы и все перечисленные старые Filters ещё остаются.
|
||||
|
||||
Точный следующий шаг: проверить git status и квоту, перенести VPN-группу (`wireguard-data-grid`, `wireguard-peers-grid`, `ipsec-users-grid`, `gre-pools-data-grid`, `gre-tunnels-data-grid`, `vxlan-data-grid`) с профильными проверками. Затем перейти к оставшимся таблицам управления/диагностики и миграции Filters. Старые зависимости удалять только после последнего потребителя. Этот блок сохранить отдельным локальным коммитом без push; ветку не выпускать.
|
||||
|
||||
## Карта миграции (исходная инвентаризация этапа 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,74 @@
|
||||
import { test, expect, type Page } from "@playwright/test"
|
||||
|
||||
const ids = Array.from({ length: 12 }, (_, i) => i + 1)
|
||||
const host = { serverId: 1, serverName: "Тестовый роутер", serverSite: "TEST", serverCountry: "RU" }
|
||||
const ospf = {
|
||||
instances: [], interfaces: [],
|
||||
neighbors: ids.map(i => ({ ...host, serverId: i, id: String(i), address: `192.0.2.${i}`,
|
||||
routerId: `192.0.2.${i}`, instance: "default", area: "0.0.0.0", areaId: "0.0.0.0",
|
||||
interface: `ether${i}`, state: "Full", uptime: "1h", stateChanges: 0, priority: i })),
|
||||
routes: ids.map(i => ({ ...host, id: String(i), destination: `10.0.${i}.0/24`,
|
||||
type: "O", cost: i, nextHop: "192.0.2.1", via: "ether1", area: "0.0.0.0" })),
|
||||
bfdSessions: ids.map(i => ({ ...host, id: String(i), localAddr: "192.0.2.100", remoteAddr: `192.0.2.${i}`,
|
||||
interface: `ether${i}`, state: "Up", uptime: "1h", multihop: false,
|
||||
multiplier: 3, txInterval: 200, rxInterval: 200, holdTime: 600, packetsRx: i, packetsTx: i, stateChanges: 0 })),
|
||||
}
|
||||
const bgp = ids.map(i => ({ ...host, id: String(i), name: `Пир ${i}`, peerIp: `192.0.2.${i}`,
|
||||
remoteAs: 65000 + i, localAs: 64512, localId: "192.0.2.100", remoteId: `192.0.2.${i}`,
|
||||
state: "Established", type: "eBGP", uptime: "1h", holdTime: 90, keepalive: 30,
|
||||
prefixesRx: i, prefixesTx: i, inputMessages: i, outputMessages: i, capabilities: ["route-refresh"], lastError: null }))
|
||||
|
||||
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
|
||||
const json = path === "/api/ospf/all" ? (empty ? { instances: [], interfaces: [], neighbors: [], routes: [], bfdSessions: [] } : ospf)
|
||||
: path === "/api/bgp/sessions" ? (empty ? [] : bgp)
|
||||
: path === "/api/auth/config" ? { required: false }
|
||||
: path === "/api/servers" ? [] : {}
|
||||
return route.fulfill({ json })
|
||||
})
|
||||
await page.goto(path)
|
||||
}
|
||||
|
||||
test("OSPF: все строки соседей, маршрутов и BFD; выбор и числовая сортировка", async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on("pageerror", e => errors.push(e.message))
|
||||
await setup(page, "/ospf")
|
||||
await page.getByRole("button", { name: "Соседи", exact: true }).click()
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(12)
|
||||
await rows.last().click()
|
||||
await expect(page.locator('tr[data-row-id="12-12"]')).toHaveAttribute("data-state", "selected")
|
||||
await page.getByRole("button", { name: "Маршруты", exact: true }).click()
|
||||
await expect(rows).toHaveCount(12)
|
||||
await page.getByRole("button", { name: "Cost", exact: true }).click()
|
||||
await expect(rows.first()).toContainText("10.0.12.0/24")
|
||||
await page.getByRole("button", { name: "BFD", exact: true }).click()
|
||||
await expect(rows).toHaveCount(12)
|
||||
await page.getByRole("button", { name: "Пакеты Rx", exact: true }).click()
|
||||
await expect(rows.first()).toContainText("192.0.2.12")
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test("BGP: 12 сессий, сортировка и раскрытие после сортировки", async ({ page }) => {
|
||||
await setup(page, "/bgp")
|
||||
const rows = page.locator("tr[data-row-id]")
|
||||
await expect(rows).toHaveCount(12)
|
||||
await page.getByRole("button", { name: "Remote AS", exact: true }).click()
|
||||
await expect(rows.first()).toContainText("65012")
|
||||
await rows.first().click()
|
||||
await expect(page.getByText("route-refresh", { exact: true })).toBeVisible()
|
||||
await rows.first().click()
|
||||
await expect(page.getByText("route-refresh", { exact: true })).toBeHidden()
|
||||
})
|
||||
|
||||
test("Пустые ответы API показывают пустые состояния BGP и OSPF", async ({ page }) => {
|
||||
await setup(page, "/bgp", true)
|
||||
await expect(page.getByText("Нет BGP-сессий", { exact: true })).toBeVisible()
|
||||
await page.goto("/ospf")
|
||||
for (const [tab, message] of [["Соседи", "Нет OSPF-соседей"], ["Маршруты", "Нет OSPF-маршрутов"], ["BFD", "BFD-сессий не обнаружено ни на одном сервере"]]) {
|
||||
await page.getByRole("button", { name: tab, exact: true }).click()
|
||||
await expect(page.getByText(message, { exact: true })).toBeVisible()
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user