fix(ui): перенести основные таблицы на ReUI v9

This commit is contained in:
Denozordec committed 2026-09-13 21:01:23 +07:00
1 parent 3d3c45e4d2
commit e66762ca2e
14 files changed
+287 -110

No files matched your search

+11 -13
View File
@@ -1,21 +1,20 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo } from "react"
import {
type ColumnDef,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table-v8"
useTable,
} from "@tanstack/react-table"
import type { Asn } from "@/lib/data"
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 { FilterIcon, NetworkIcon } from "lucide-react"
@@ -26,7 +25,7 @@ interface AsnsDataGridProps {
}
function AsnsDataGrid({ asns, isLoading, pagination = false }: AsnsDataGridProps) {
const columns = useMemo<ColumnDef<Asn>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, Asn>[]>(
() => [
{
id: "asn",
@@ -115,12 +114,11 @@ function AsnsDataGrid({ asns, isLoading, pagination = false }: AsnsDataGridProps
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: !pagination,
data: asns,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
...(pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
getRowId: (row) => row.id,
})
+12 -12
View File
@@ -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 {
CheckIcon,
@@ -77,7 +77,7 @@ function CommunitiesDataGrid({
onSelect,
onCopy,
}: CommunitiesDataGridProps) {
const columns = useMemo<ColumnDef<CommunityRow>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, CommunityRow>[]>(
() => [
{
id: "value",
@@ -222,15 +222,15 @@ function CommunitiesDataGrid({
)
const rowSelection = useMemo(
() => (selectedId ? { [selectedId]: true } : {}),
() => (selectedId ? { [selectedId]: true as const } : {}),
[selectedId],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
data: communities,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getRowId: (row) => row.id,
enableRowSelection: true,
state: { rowSelection },
+11 -13
View File
@@ -1,20 +1,19 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo, type ReactNode } from "react"
import {
type ColumnDef,
getCoreRowModel,
getExpandedRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table-v8"
import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell"
useTable,
} from "@tanstack/react-table"
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 { InboxIcon } from "lucide-react"
@@ -47,7 +46,7 @@ function CompactDataGrid<T extends { id: string }>({
onRowClick,
getExpandedContent,
}: CompactDataGridProps<T>) {
const columnDefs = useMemo<ColumnDef<T>[]>(
const columnDefs = useMemo<ColumnDef<DataGridFeatures, T>[]>(
() =>
columns.map((col, index) => ({
id: col.id,
@@ -81,12 +80,11 @@ function CompactDataGrid<T extends { id: string }>({
[columns, getExpandedContent],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
data,
columns: columnDefs,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
...(getExpandedContent ? { getExpandedRowModel: getExpandedRowModel() } : {}),
getRowId: (row) => row.id,
...(getExpandedContent ? { getRowCanExpand: () => true } : {}),
})
+11 -13
View File
@@ -1,21 +1,20 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo } from "react"
import {
type ColumnDef,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table-v8"
useTable,
} from "@tanstack/react-table"
import type { Domain } from "@/lib/data"
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 { FilterIcon, GlobeIcon } from "lucide-react"
@@ -26,7 +25,7 @@ interface DomainsDataGridProps {
}
function DomainsDataGrid({ domains, isLoading, pagination = false }: DomainsDataGridProps) {
const columns = useMemo<ColumnDef<Domain>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, Domain>[]>(
() => [
{
id: "domain",
@@ -124,12 +123,11 @@ function DomainsDataGrid({ domains, isLoading, pagination = false }: DomainsData
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: !pagination,
data: domains,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
...(pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
getRowId: (row) => row.id,
})
+11 -13
View File
@@ -1,21 +1,20 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo } from "react"
import {
type ColumnDef,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table-v8"
useTable,
} from "@tanstack/react-table"
import type { IpRange } from "@/lib/data"
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 { FilterIcon, NetworkIcon } from "lucide-react"
@@ -26,7 +25,7 @@ interface IpRangesDataGridProps {
}
function IpRangesDataGrid({ ipRanges, isLoading, pagination = false }: IpRangesDataGridProps) {
const columns = useMemo<ColumnDef<IpRange>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, IpRange>[]>(
() => [
{
id: "cidr",
@@ -126,12 +125,11 @@ function IpRangesDataGrid({ ipRanges, isLoading, pagination = false }: IpRangesD
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: !pagination,
data: ipRanges,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
...(pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
getRowId: (row) => row.id,
})
@@ -2,6 +2,7 @@
import type { ReactNode } from "react"
import type { Table } from "@tanstack/react-table"
import { DATA_GRID_LABELS_RU } from "@/lib/data-grid-labels-ru"
import {
DataGrid,
DataGridContainer,
@@ -44,6 +45,7 @@ function DataGridShell<TData extends object>({
return (
<DataGrid
className="w-full"
i18n={{ labels: DATA_GRID_LABELS_RU }}
table={table}
recordCount={recordCount}
isLoading={isLoading}
@@ -1,15 +1,17 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo } from "react"
import { type ColumnDef, getCoreRowModel, useReactTable } from "@tanstack/react-table-v8"
import { type ColumnDef, useTable } from "@tanstack/react-table"
import type { FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
import { ArrowDownIcon, ArrowUpIcon, MinusIcon } from "lucide-react"
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"
} from "@/components/data-grids/shared/data-grid-layout"
import { fmtRate } from "@/lib/fmt-rate"
import { cn } from "@/lib/utils"
@@ -61,7 +63,7 @@ function TrafficFlowsDataGrid({
rows: FlowTalkerDto[]
emptyHint?: string
}) {
const columns = useMemo<ColumnDef<FlowTalkerDto>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, FlowTalkerDto>[]>(
() => [
{
id: "client",
@@ -169,10 +171,12 @@ function TrafficFlowsDataGrid({
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
manualSorting: true,
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row, i) => `${row.serverId}-${row.src}-${row.dst}-${row.proto}-${row.srcPort}-${row.dstPort}-${i}`,
})
@@ -1,31 +1,30 @@
"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 { Server } from "@/lib/data"
import { Flag } from "@/components/flag"
import { Sparkline } from "@/components/sparkline"
import { StatusDot } from "@/components/status-dot"
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 {
AlertCircleIcon,
ClockIcon,
CpuIcon,
HardDriveIcon,
ServerIcon,
SearchIcon,
ThermometerIcon,
} from "lucide-react"
@@ -114,7 +113,7 @@ interface UptimeResourcesDataGridProps {
}
function UptimeResourcesDataGrid({ rows }: UptimeResourcesDataGridProps) {
const columns = useMemo<ColumnDef<UptimeResourceRow>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, UptimeResourceRow>[]>(
() => [
{
id: "name",
@@ -348,11 +347,11 @@ function UptimeResourcesDataGrid({ rows }: UptimeResourcesDataGridProps) {
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getRowId: (row) => row.serverId,
})
@@ -1,21 +1,21 @@
"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 { Server } from "@/lib/data"
import { Flag } from "@/components/flag"
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 { cn } from "@/lib/utils"
import { ArrowRightIcon, RefreshCwIcon } from "lucide-react"
@@ -43,7 +43,7 @@ interface UptimeSpeedHistoryDataGridProps {
}
function UptimeSpeedHistoryDataGrid({ runs, servers }: UptimeSpeedHistoryDataGridProps) {
const columns = useMemo<ColumnDef<SpeedTestRunRow>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, SpeedTestRunRow>[]>(
() => [
{
id: "startedAt",
@@ -221,11 +221,11 @@ function UptimeSpeedHistoryDataGrid({ runs, servers }: UptimeSpeedHistoryDataGri
[servers],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
data: runs,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getRowId: (row) => row.id,
initialState: { sorting: [{ id: "startedAt", desc: true }] },
})
+15 -9
View File
@@ -1,7 +1,9 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo, useState } from "react"
import { type ColumnDef, getCoreRowModel, useReactTable } from "@tanstack/react-table-v8"
import { type ColumnDef, useTable } from "@tanstack/react-table"
import type { FlowAnalyticsDto, FlowBreakdownRow, FlowEntityCard, FlowPathRow, FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
import { ArrowDownIcon, ArrowUpIcon, GitBranchIcon, GlobeIcon, LayersIcon, NetworkIcon, RouteIcon, ShieldIcon, UsersIcon } from "lucide-react"
import { Badge } from "@/components/reui/badge"
@@ -10,12 +12,12 @@ import { TrafficRxTxChart } from "@/components/reui-kit/traffic-rx-tx-chart"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
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"
} from "@/components/data-grids/shared/data-grid-layout"
import { TrafficFlowsDataGrid } from "@/components/data-grids/traffic-flows-data-grid"
import { FlowTrafficMap } from "@/components/traffic/flow-traffic-map"
import { StatusDot } from "@/components/status-dot"
@@ -142,7 +144,7 @@ function FlowBreakdownGrid({
country?: boolean
onPick?: (row: FlowBreakdownRow) => void
}) {
const columns = useMemo<ColumnDef<FlowBreakdownRow>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, FlowBreakdownRow>[]>(
() => [
{
id: "label",
@@ -195,10 +197,12 @@ function FlowBreakdownGrid({
[country],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
manualSorting: true,
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.id,
})
@@ -221,7 +225,7 @@ function FlowPathsGrid({
empty?: string
onPick?: (row: FlowPathRow) => void
}) {
const columns = useMemo<ColumnDef<FlowPathRow>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, FlowPathRow>[]>(
() => [
{
id: "client",
@@ -290,10 +294,12 @@ function FlowPathsGrid({
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
manualSorting: true,
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.id,
})
+10 -6
View File
@@ -1,15 +1,17 @@
"use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo, useState } from "react"
import { type ColumnDef, getCoreRowModel, useReactTable } from "@tanstack/react-table-v8"
import { type ColumnDef, useTable } from "@tanstack/react-table"
import type { FlowMapEdge } from "@mmapp/contracts/traffic-flow"
import { Frame, FramePanel } from "@/components/reui/frame"
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"
} from "@/components/data-grids/shared/data-grid-layout"
import { Flag } from "@/components/flag"
import { fmtRate } from "@/lib/fmt-rate"
@@ -50,7 +52,7 @@ function FlowTrafficMap({
return { srcPos, dstPos, maxBytes }
}, [edges])
const columns = useMemo<ColumnDef<FlowMapEdge>[]>(
const columns = useMemo<ColumnDef<DataGridFeatures, FlowMapEdge>[]>(
() => [
{
id: "from",
@@ -106,10 +108,12 @@ function FlowTrafficMap({
[],
)
const table = useReactTable({
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
manualSorting: true,
data: edges,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row, i) => `${row.fromId}-${row.toCountry}-${i}`,
})
+27 -2
View File
@@ -17,7 +17,7 @@
| 0 | Исходная база, карта миграции, проверки и обзор экранов | Завершён 2026-09-13 |
| 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 |
| 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки |
| 3 | Основные таблицы и справочники | Следующий, отдельное окно |
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Не начат |
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат |
| 6 | Навигация и дашборд | Не начат |
@@ -181,7 +181,32 @@ flowchart LR
Следующий шаг: проверить квоту и состояние файлов; выполнить только этап 3 (обзор, мониторинг, трафик, статистика и справочники) в этой ветке, перечислить оставшихся legacy-потребителей и сделать локальный коммит без push. Перед миграцией виртуализированных таблиц добавить профильные проверки указанного выше риска контекста. Этапы 2–4 не выпускать до общей регрессии этапа 4.
## Карта миграции
## Этап 3 — основные таблицы и справочники
Перенесены десять прямых потребителей: `asns-data-grid`, `domains-data-grid`, `ip-ranges-data-grid`, `communities-data-grid`, `compact-data-grid`, `uptime-resources-data-grid`, `uptime-speed-history-data-grid`, `traffic-flows-data-grid`, `traffic/flow-analytics-panel`, `traffic/flow-traffic-map`. Через CompactDataGrid новую основу получили таблицы дашборда, статистики и снимков состояния. Серверы уже перенесены на этапе 2.
Используются `useTable`, `dataGridFeatures` и типы v9. Сохранены доменные props, стабильные идентификаторы, раскрытие и обработчики действий. Для списков без панели явно отключена клиентская пагинация; для каталогов EvoBGP она сохранена. Таблицы потоков сохраняют исходный порядок данных. Состояние выбора Communities приведено к типу v9. Общая оболочка получила русские служебные подписи через документированный `i18n`, без изменения установленных примитивов ReUI.
| Референс | Схема сохранённого сценария |
| --- | --- |
| [Data Grid Filtering 2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Поиск → страница таблицы → сортировка / выбор строки → детали |
| [API Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp) | Типизированные данные → useTable v9 → DataGridShell → таблица и опциональная пагинация |
Проверки: TypeScript и целевой ESLint — без ошибок и предупреждений. 17 браузерных тестов серверов прошли. Добавлены 6 сценариев `tests/e2e/migrated-tables.spec.ts`: пагинация и поиск с последней страницы в трёх справочниках, числовая сортировка через границу страницы, выбор одной из 12 Communities, все 12 строк статистики и переход в URL-срез. Первый старт Next.js превысил 120 секунд; повторный старт успешен. Два теста сначала упали из-за неточных селекторов, исправлены и повторно прошли. Все запросы перехватывались HTTP-моками.
ReUI audit checklist получен. Сохранены Frame/base-nova, композиция и типизированные источники; новые props сверены с установленными файлами. Просмотрен экран Communities: таблица и выбранная строка отображаются, но правая панель деталей обрезается при ширине 1440 px. Исправить размещение таблицы/деталей при профильной переработке экранов; общую доступность и адаптивность не считать принятой. Существующие неработающие действия справочников также не входят в приёмку миграции. Браузерные проверки всех комбинаций мониторинга, traffic-map и snapshot-деталей остаются для общей регрессии этапа 4 и профильных этапов 7–10. Виртуализатор эти десять компонентов не используют; нагрузочный профиль общей основы остаётся этапу 12.
Production build прошёл: `npm run build`, 34 статические страницы. Последняя проверка квоты перед завершением блока: использовано 37% пятичасового окна и 50% недельного; это общий расход аккаунта, не оценка стоимости этапа.
### Остаток для этапа 4
- Доменные legacy-таблицы: backups, bgp-sessions, certificates, data-collection-scheduler, filters, firewall-rules, firewall-scenario-rules, gre-pools, gre-tunnels, ospf-bfd, ospf-neighbors, ospf-routes, probes-schedule, probes-speed-probes, recursive-routes, route-optimizer-comm-recs, route-optimizer-full-routes, route-optimizer-ospf-preview, route-optimizer-wan-matrix, subusers, users, vxlan, wireguard, wireguard-peers, ipsec-users-grid.
- Общая основа: `components/data-table.tsx`, `components/data-grids/shared/legacy-*`, `components/reui/legacy-data-grid/*`; ссылки на legacy layout в DataPageCard и DataPageToolbar. Проверить отсутствие потребителей перед удалением.
- Старые Filters: статистика, BGP, backups, backups-history и fallback DataPageToolbar; старые определения полей/вычислитель. Статистика передаёт в API только равенство скалярных срезов: при переносе нельзя принимать OR/отрицание и молча превращать их в равенство. Сохранять незавершённый черновик отдельно от URL.
Следующий шаг: проверить квоту и git status; начать этап 4 в `codex/reui-v9-migration`, переносить оставшихся потребителей ограниченными группами, затем удалить v8/старые Filters и выполнить общую регрессию. Ветку до этой проверки не выпускать. Коммит этапа 3 — только локальный, без push; настройки skills/AGENTS/.codex других работ не включать.
## Карта миграции (исходная инвентаризация этапа 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.
+35
View File
@@ -0,0 +1,35 @@
import type { DataGridI18nLabels } from "@/components/reui/data-grid/data-grid-i18n"
export const DATA_GRID_LABELS_RU: DataGridI18nLabels = {
sortAscending: "По возрастанию",
sortDescending: "По убыванию",
pinColumnStart: "Закрепить слева",
pinColumnEnd: "Закрепить справа",
moveColumnStart: "Переместить влево",
moveColumnEnd: "Переместить вправо",
columnsMenu: "Столбцы",
unpinColumn: title => `Открепить столбец ${title}`,
toggleColumns: "Настроить столбцы",
rowCreate: "Добавить строку",
pinRow: "Закрепить строку",
unpinRow: "Открепить строку",
selectRow: "Выбрать строку",
selectAll: "Выбрать все",
expandRow: "Раскрыть строку",
collapseRow: "Свернуть строку",
dragToReorder: "Перетащите для изменения порядка",
dragToReorderRow: "Перетащите для изменения порядка строк",
reorderingUnavailable: "Изменение порядка недоступно",
loading: "Загрузка…",
empty: "Нет данных",
allRowsLoaded: "Все записи загружены",
rowsPerPage: "Строк на странице",
paginationInfo: ({ from, to, count }) => `${from}–${to} из ${count}`,
previousPage: "Предыдущая страница",
nextPage: "Следующая страница",
goToPage: page => `Перейти на страницу ${page}`,
paginationEllipsis: "…",
filterSelectedCount: count => `Выбрано: ${count}`,
filterNoResults: "Ничего не найдено",
filterClear: "Очистить фильтры",
}
+110
View File
@@ -0,0 +1,110 @@
import { test, expect, type Page } from "@playwright/test"
import type { EvoBgpCatalogSnapshot } from "../../lib/evobgp-context"
import { statisticsDtoSchema } from "../../packages/contracts/src/statistics"
const catalog: EvoBgpCatalogSnapshot = {
fetchedAt: "2026-09-13T00:00:00Z", modules: [],
asns: Array.from({ length: 12 }, (_, i) => ({
id: String(i + 1), asn: `AS${65001 + i}`, org: `Организация ${i + 1}`,
country: "RU", prefixes: i + 1, filter: "", updated: "2026-09-13", enabled: true,
})),
domains: Array.from({ length: 12 }, (_, i) => ({
id: String(i + 1), domain: `test${i + 1}.example`, resolvedIp: `192.0.2.${i + 1}`,
asn: "AS65001", purpose: "Тест", filter: "", updated: "2026-09-13", enabled: true,
})),
ipRanges: Array.from({ length: 12 }, (_, i) => ({
id: String(i + 1), cidr: `192.0.${i + 1}.0/24`, asn: "AS65001", country: "RU",
purpose: "Тест", filter: "", updated: "2026-09-13", enabled: true,
})),
communities: Array.from({ length: 12 }, (_, i) => ({
id: String(i + 1), value: `65001:${i + 1}`, name: `Community ${i + 1}`,
description: `Описание ${i + 1}`, type: "standard", filterIds: [],
serverCount: i + 1, prefixCount: i + 1, action: "permit", enabled: true,
})),
}
async function setup(page: Page, path: string) {
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/auth/config" ? { required: false }
: path === "/api/evobgp/settings" ? { enabled: true, baseUrl: "", secretConfigured: true }
: path === "/api/evobgp/catalog" ? catalog
: path === "/api/servers" ? [] : {}
return route.fulfill({ json })
})
await page.goto(path)
}
for (const [path, placeholder, last] of [
["/asns", "Поиск по ASN, имени, префиксам…", "AS65012"],
["/domains", "Поиск по домену…", "test12.example"],
["/ip-ranges", "Поиск по CIDR, ASN…", "192.0.12.0/24"],
]) {
test(`${path}: страницы и поиск с последней страницы`, async ({ page }) => {
const errors: string[] = []
page.on("pageerror", e => errors.push(e.message))
await setup(page, path)
const rows = page.locator("tr[data-row-id]")
await expect(page.getByText("1–10 из 12", { exact: true })).toBeVisible()
await expect(rows).toHaveCount(10)
await page.getByRole("button", { name: "Следующая страница", exact: true }).click()
await expect(rows).toHaveCount(2)
await expect(rows.last()).toContainText(last)
await page.getByPlaceholder(placeholder).fill(last)
await expect(rows).toHaveCount(1)
await expect(rows.first()).toContainText(last)
await expect(page.getByText("1–1 из 1", { exact: true })).toBeVisible()
await page.getByPlaceholder(placeholder).fill("несуществующее значение")
await expect(rows).toHaveCount(0)
await page.getByPlaceholder(placeholder).fill("")
await expect(rows).toHaveCount(10)
expect(errors).toEqual([])
})
}
test("ASN: числовая сортировка охватывает обе страницы", async ({ page }) => {
await setup(page, "/asns")
await expect(page.getByText("1–10 из 12", { exact: true })).toBeVisible()
await page.getByRole("button", { name: "Префиксов", exact: true }).click()
await expect(page.locator("tr[data-row-id]").first()).toContainText("AS65012")
await page.getByRole("button", { name: "Следующая страница", exact: true }).click()
await expect(page.locator("tr[data-row-id]").last()).toContainText("AS65001")
})
test("Communities: все 12 строк и выбор деталей", async ({ page }) => {
await setup(page, "/communities")
await expect(page.locator("tr[data-row-id]")).toHaveCount(12)
await page.locator('tr[data-row-id="12"]').click()
await expect(page.locator('tr[data-row-id="12"]')).toHaveAttribute("data-state", "selected")
await expect(page.getByText("Описание 12", { exact: true })).toHaveCount(2)
await expect(page.getByText("Описание 12", { exact: true }).last()).toBeVisible()
await page.screenshot({ path: "test-results/communities-migrated.png", fullPage: true })
})
test("Статистика: CompactDataGrid сохраняет 12 строк, сортировку и переход в срез", async ({ page }) => {
const dto = statisticsDtoSchema.parse({
from: "2026-09-01", to: "2026-09-13", grain: "day",
kpis: { bytes: 7800, packets: 78, avgBps: 100, users: 12, servers: 1, ifaces: 1, topCountry: "RU", topService: "https" },
series: [], servers: [], interfaces: [], countries: [], services: [], asns: [],
users: Array.from({ length: 12 }, (_, i) => ({
id: `user-${i + 1}`, label: `Пользователь ${i + 1}`, bytes: (i + 1) * 100,
packets: i + 1, bps: i + 1, percent: i + 1,
})),
})
await page.route("**/api/statistics?**", route => route.fulfill({ json: dto }))
// The general API handler falls back to the previously registered statistics route.
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
await page.route("**/api/**", route => {
const path = new URL(route.request().url()).pathname
if (path === "/api/statistics") return route.fallback()
return route.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [] : {} })
})
await page.goto("/statistics?from=2026-09-01&to=2026-09-13&dim=users")
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(12)
await page.getByRole("button", { name: "Байты", exact: true }).first().click()
await expect(rows.first()).toContainText("Пользователь 12")
await rows.first().click()
await expect(page).toHaveURL(/userId=user-12/)
})