diff --git a/components/data-grids/data-collection-scheduler-data-grid.tsx b/components/data-grids/data-collection-scheduler-data-grid.tsx index 8e575021..ed193573 100644 --- a/components/data-grids/data-collection-scheduler-data-grid.tsx +++ b/components/data-grids/data-collection-scheduler-data-grid.tsx @@ -1,22 +1,23 @@ "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" + useTable, +} from "@tanstack/react-table" import type { SchedulerJobStatusDto } from "@/lib/scheduler-settings" import { FormToggle } from "@/components/form-kit" import { Badge } from "@/components/reui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -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 { cn } from "@/lib/utils" import { RefreshCwIcon } from "lucide-react" @@ -44,7 +45,7 @@ interface DataCollectionSchedulerDataGridProps { } function DataCollectionSchedulerDataGrid({ rows }: DataCollectionSchedulerDataGridProps) { - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { id: "task", @@ -185,6 +186,7 @@ function DataCollectionSchedulerDataGrid({ rows }: DataCollectionSchedulerDataGr className="h-8" disabled={r.job?.running || r.runNowLoading} onClick={r.onRunNow} + aria-label={`Запустить: ${r.label}`} > @@ -200,10 +202,12 @@ function DataCollectionSchedulerDataGrid({ rows }: DataCollectionSchedulerDataGr [], ) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: true, + manualSorting: true, data: rows, columns, - getCoreRowModel: getCoreRowModel(), getRowId: (row) => row.id, }) diff --git a/components/data-grids/probes-schedule-data-grid.tsx b/components/data-grids/probes-schedule-data-grid.tsx index fecefef4..4e3d429f 100644 --- a/components/data-grids/probes-schedule-data-grid.tsx +++ b/components/data-grids/probes-schedule-data-grid.tsx @@ -1,22 +1,22 @@ "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 { cn } from "@/lib/utils" import { FormToggle } from "@/components/form-kit" -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 { ClockIcon, Trash2Icon } from "lucide-react" @@ -59,7 +59,7 @@ function ProbesScheduleDataGrid({ [serverOptions], ) - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { id: "enabled", @@ -196,11 +196,11 @@ function ProbesScheduleDataGrid({ [onDelete, onToggleEnabled, serverMap, tunnelName], ) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: true, data: rules, columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.id, }) diff --git a/components/data-grids/probes-speed-probes-data-grid.tsx b/components/data-grids/probes-speed-probes-data-grid.tsx index 0a953e3a..24851237 100644 --- a/components/data-grids/probes-speed-probes-data-grid.tsx +++ b/components/data-grids/probes-speed-probes-data-grid.tsx @@ -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 { ClockIcon } from "lucide-react" @@ -41,7 +41,7 @@ interface ProbesSpeedProbesDataGridProps { } function ProbesSpeedProbesDataGrid({ rows, serverName }: ProbesSpeedProbesDataGridProps) { - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { id: "src", @@ -144,11 +144,11 @@ function ProbesSpeedProbesDataGrid({ rows, serverName }: ProbesSpeedProbesDataGr [serverName], ) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: true, data: rows, columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), getRowId: (row) => row.id, }) diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 1084ecd3..57ee9da6 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -280,6 +280,18 @@ TypeScript и целевой ESLint прошли. ReUI checklist получен: Production build блока 7: `npm run build` прошёл, 34 статические страницы. Это проверка сборки, не замена общей регрессии этапа 4. +## Этап 4 — блок 8: сборщик и расписания проб (2026-09-14) + +Перенесены `data-collection-scheduler-data-grid`, `probes-schedule-data-grid`, `probes-speed-probes-data-grid` на установленный ReUI Data Grid/TanStack v9: `useTable`, `dataGridFeatures`, общая современная оболочка и заголовки. Сохранены идентификаторы строк, обработчики и порядок задач сборщика; автоматическая пагинация отключена. Кнопка запуска задачи получила доступное имя с названием задачи. + +Проверки: TypeScript без incremental и ESLint изменённых файлов; три сценария Playwright — 12 speed-probes и сортировка источника, все 11 задач сборщика и POST выбранного `alert_engine/run-now`, сортировка и удаление выбранного правила демонстрационного расписания. Финальный браузерный прогон: 3 passed. Исправлены неполные фоновые HTTP-фикстуры EvoBGP, GeoIP и Traffic Flow; повторный прогон без предупреждений controlled/uncontrolled. Live-ветки проверены с подменой HTTP, обычное расписание — отдельно во встроенном mock-режиме. Реальное исполнение задач и RouterOS не проверялись. + +Первичная проверка TypeScript до браузерного запуска прошла. Финальная повторная проверка после Playwright заблокирована двумя TS2304 в сгенерированном `.next/dev/types/validator.ts` (строки 379 и 388: отсутствует `RouteHandlerConfig`). `next typegen` успешно обновил типы, но ошибку dev-кэша не устранил. Исходники и конфигурация TypeScript ради обхода ошибки не менялись. При продолжении проверить dev-кэш и повторить tsc; финальный TypeScript не считать пройденным. ESLint изменённых файлов прошёл. + +Применены правила установленного ReUI и ранее полученный checklist: публичный API и существующая композиция сохранены, полная визуальная/адаптивная приёмка остаётся последующим этапам. Общая основа не менялась; полная регрессия — после завершения миграции. + +Квота: использовано 46% на старте, 81% перед финальными проверками. Остаток ниже резерва 25%, новые блоки не начинать. Следующий шаг — `recursive-routes-data-grid` и четыре `route-optimizer-*-data-grid`; далее `data-table`, общие legacy-потребители, старые Filters и удаление v8 с общей регрессией. Этап 4 остаётся открытым; сохранить блок локальным коммитом без 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. diff --git a/tests/e2e/collector-probes-tables.spec.ts b/tests/e2e/collector-probes-tables.spec.ts new file mode 100644 index 00000000..fa59748a --- /dev/null +++ b/tests/e2e/collector-probes-tables.spec.ts @@ -0,0 +1,68 @@ +import { test, expect, type Page } from "@playwright/test" +import type { SpeedProbeApiRow } from "../../components/data-grids/probes-speed-probes-data-grid" + +async function setup(page: Page, path: string) { + const probes: SpeedProbeApiRow[] = Array.from({ length: 12 }, (_, i) => ({ + id: String(i + 1), srcServerId: `Источник ${i + 1}`, dstServerId: "Цель", srcInterface: "ether1", + dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, + lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null, + })) + const calls: string[] = [] + await page.route("**/health", route => route.fulfill({ json: { ok: true } })) + await page.route("**/api/**", route => { + const path = new URL(route.request().url()).pathname + if (route.request().method() === "POST") calls.push(path) + return route.fulfill({ json: path === "/api/uptime/speed-probes" ? { probes } + : path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [] + : path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false } + : path === "/api/traffic/flow/settings" ? { + enabled: false, collectorIp: "10.0.0.1", flowListenPort: 2055, wgListenPort: 51820, + prefix: "10.0.0.0/24", publicEndpoint: "", hostPublicKey: "", hasHostPrivateKey: false, + hubServerId: null, retentionHours: 24, topN: 10, mapServiceMinSharePct: 1, + lastDatagramAt: null, lastExporterIp: null, lastError: null, packetsReceived: 0, + listenerBound: false, listenerAddress: null, peers: [], + } + : path === "/api/geoip" ? { ready: false, running: false, countryFile: "", asnFile: "", countryLoaded: false, asnLoaded: false, dir: "storage/geoip", settings: { enabled: false, updateIntervalSec: 86400, lastCheckAt: null, lastSuccessAt: null, lastError: null, countryBuildAt: null, asnBuildAt: null, updatedAt: "2026-09-14" } } + : path === "/api/scheduler/runs" ? { runs: [] } + : path.endsWith("/settings") || path.endsWith("/renew-settings") ? { + enabled: true, intervalSec: 300, retentionDays: 7, probeIntervalSec: 15, speedIntervalSec: 60, + renewBeforeDays: 30, scheduler: { jobs: [] }, + } : {} }) + }) + await page.goto(path) + return calls +} + +test("Speed probes: 12 записей и сортировка источника", async ({ page }) => { + await setup(page, "/probes") + await page.getByRole("button", { name: "Расписание", exact: true }).click() + const rows = page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(12) + await page.getByRole("button", { name: "Источник", exact: true }).click() + await expect(rows.first()).toContainText("Источник 1") + await page.getByRole("button", { name: "Источник", exact: true }).click() + await expect(rows.first()).toContainText("Источник 12") +}) + +test("Сбор данных: все 11 задач и запуск выбранной задачи через API", async ({ page }) => { + const calls = await setup(page, "/data-collection") + const rows = page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(11) + await expect(rows.last()).toContainText("Оповещения") + await page.getByRole("button", { name: "Запустить: Оповещения", exact: true }).click() + await expect.poll(() => calls).toContain("/api/scheduler/jobs/alert_engine/run-now") + await expect(rows).toHaveCount(11) +}) + +test("Демо-расписание: сортировка интервала и удаление выбранного правила", async ({ page }) => { + await page.addInitScript(() => localStorage.setItem("routerlists:data-source", "mock")) + await setup(page, "/probes") + await page.getByRole("button", { name: "Расписание", exact: true }).click() + const rows = page.locator("tr[data-row-id]") + await expect(rows).toHaveCount(3) + await page.getByRole("button", { name: "Интервал", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "r3") + await rows.first().getByRole("button", { name: "Удалить правило", exact: true }).click() + await expect(rows).toHaveCount(2) + await expect(page.locator('tr[data-row-id="r3"]')).toHaveCount(0) +})