From a16bfae805e1c9a2c15f8613b8a1a92eb768edc0 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 18:13:45 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D1=8B=20=D1=81=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route-optimizer-full-routes-data-grid.tsx | 43 +++++++++++++----- docs/ux-ui-performance.md | 5 +- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 9 ++++ .../optimizer-routes-pages-2026-10-04.json | 9 ++++ .../screens/optimizer-routes-pages-320.png | Bin 0 -> 54911 bytes tests/e2e/large-optimizer-routes.spec.ts | 43 ++++++++++++++++-- 8 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 docs/ux-ui/acceptance/optimizer-routes-pages-2026-10-04.json create mode 100644 docs/ux-ui/screens/optimizer-routes-pages-320.png diff --git a/components/data-grids/route-optimizer-full-routes-data-grid.tsx b/components/data-grids/route-optimizer-full-routes-data-grid.tsx index d306c9ca..e9ab8606 100644 --- a/components/data-grids/route-optimizer-full-routes-data-grid.tsx +++ b/components/data-grids/route-optimizer-full-routes-data-grid.tsx @@ -1,8 +1,8 @@ "use client" -import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" +import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { type ColumnDef, useTable, @@ -76,13 +76,14 @@ function RouteOptimizerFullRoutesDataGrid({ () => [ { id: "index", + size: 112, header: () => # Маршрут, enableSorting: false, cell: ({ row }) => { const isBest = row.original.id === bestId || row.original._index === 0 return (
- + {row.original._index + 1} {isBest && ( @@ -97,6 +98,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "wanJh", + size: 264, header: () => WAN → JH, enableSorting: false, cell: ({ row }) => { @@ -118,6 +120,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "jhExit", + size: 240, header: () => JH → Exit, enableSorting: false, cell: ({ row }) => { @@ -139,6 +142,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "totalPing", + size: 144, accessorFn: (row) => row.hw.pingMs + row.je.pingMs, header: ({ column }) => ( @@ -168,6 +172,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "bw", + size: 144, header: () => ( BW (мин) @@ -192,6 +197,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "score", + size: 80, accessorKey: "score", header: ({ column }) => ( @@ -209,6 +215,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "prob", + size: 104, accessorKey: "probabilityOptimal", header: ({ column }) => ( @@ -229,6 +236,7 @@ function RouteOptimizerFullRoutesDataGrid({ }, { id: "confidence", + size: 176, accessorKey: "confidence", header: ({ column }) => ( @@ -250,28 +258,41 @@ function RouteOptimizerFullRoutesDataGrid({ const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["index"], end: [] } }, data: indexed, columns, getRowId: (row) => row.id, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(indexed.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [indexed.length, pageIndex, pageSize, table]) + return ( -
+
50 || pageSize !== 50)} tableClassNames={{ headerRow: "border-b bg-muted/20", bodyRow: cn("group/row hover:bg-muted/30", "has-[[data-route-best=true]]:bg-success/5"), }} - tableLayout={{ dense: true }} - /> + tableLayout={{ dense: true, width: "fixed", columnsPinnable: true }} + > + + + + {routes.length > 5 && ( )} -
+ ) } diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 6b96f2dc..52779c10 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -15,7 +15,7 @@ ## Оставшиеся специализированные grids -Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —8 файлов после12.3.53. Это не8 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API. +Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —7 файлов после12.3.57. Это не7 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API. | Файл | Источник и необходимая проверка | | --- | --- | @@ -23,7 +23,6 @@ | [gre-pools-data-grid.tsx](../components/data-grids/gre-pools-data-grid.tsx) | В live displayPools=[]; только mockGrePools в demo. Приоритета live1000 нет. | | [probes-schedule-data-grid.tsx](../components/data-grids/probes-schedule-data-grid.tsx) | Только demo ScheduleTab/INIT_RULES3; live использует ProbesSpeedSchedule. Demo список расширяется пользователем; не доказательство live API. | | [route-optimizer-comm-recs-data-grid.tsx](../components/data-grids/route-optimizer-comm-recs-data-grid.tsx) | Live commRecs slice(0,24) на HomeCard, не на страницу. Проверить количество HomeCard и честность ограничения рекомендаций. | -| [route-optimizer-full-routes-data-grid.tsx](../components/data-grids/route-optimizer-full-routes-data-grid.tsx) | Live сочетания WAN×jumpHost×exitNode на HomeCard; ограничение DOM не устраняет стоимость расчёта полного массива. | | [route-optimizer-wan-matrix-data-grid.tsx](../components/data-grids/route-optimizer-wan-matrix-data-grid.tsx) | Live сочетания WAN×jumpHost на HomeCard; динамические столбцы/много HomeCard требуют собственного профиля. | | [subusers-data-grid.tsx](../components/data-grids/subusers-data-grid.tsx) | Поиск импортов/JSX в app/components не нашёл потребителя: компонент экспортируется, но не подключён. Не доказывает дефект экрана /users. | | [traffic-flows-data-grid.tsx](../components/data-grids/traffic-flows-data-grid.tsx) | FlowAnalyticsPanel/conversationsList: backend traffic-flow-analytics.ts top=min(50,max(10,settings.topN)), slice(0,top). Это top slice, не полный каталог/серверная pagination. Проверить сообщение об ограничении50 и связывание с графиками. | @@ -107,3 +106,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.55:1000 HomeRouterCard →5/1870 page DOM/1606ms dev ready, full search/URL/refresh/cache/clamp/320px/axe/pin preserved.23+8 main/8 context/type/lint/build. Полный CPU расчёт и1000 native device options остаются; далее1000 full routes. 12.3.56:WAN10×JH10×Exit10/21 servers/4261 bytes →1000 expanded full-route rows/33049 table DOM/33780 page DOM/3738ms dev ready.1 browser/test lint; baseline, далее12.3.57. + +12.3.57:1000 full routes→50 rows/1699 table nodes/2461 page DOM/1853ms dev ready.6+1 browser/type/lint/build, global sort/index/page20/refresh/clamp/320px/axe.7 manualPagination:true, далее WAN matrix/CPU/24 commRecs. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 03a9e8dc..6a96b184 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-04: 12.3.56 baseline1000 full routes сохранён:33049 table DOM/33780 page DOM/3738ms dev ready;1 browser/test lint. Далее12.3.57 — client pagination раскрытого списка/полная сортировка/refresh/clamp/320px; затем WAN matrix/общая регрессия. Внешняя12.5 отдельно. +Актуальное продолжение 2026-10-04: 12.3.57 принят локально:1000 full routes→50/1699 table DOM/2461 page DOM, global sort/индекс/page20/refresh/clamp/320px/axe.6+1 browser/type/lint/build. Далее12.3.58 — профиль WAN matrix/динамических столбцов и CPU комбинирования; честность24 рекомендаций/полная регрессия после delta.7 specialized manualPagination:true остаются. Внешняя12.5 отдельно. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 1cd5edbd..be5f3f09 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-04: 12.3.56 baseline1000 full routes сохранён:33049 table DOM/33780 page DOM/3738ms dev ready;1 browser/test lint. Далее12.3.57 — client pagination раскрытого списка/полная сортировка/refresh/clamp/320px; затем WAN matrix/общая регрессия. Внешняя12.5 отдельно. +Актуальное продолжение 2026-10-04: 12.3.57 принят локально:1000 full routes→50/1699 table DOM/2461 page DOM, global sort/индекс/page20/refresh/clamp/320px/axe.6+1 browser/type/lint/build. Далее12.3.58 — профиль WAN matrix/динамических столбцов и CPU комбинирования; честность24 рекомендаций/полная регрессия после delta.7 specialized manualPagination:true остаются. Внешняя12.5 отдельно. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 557ec250..6c33dd29 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -651,3 +651,12 @@ OptimizerHomesList показывает5 карточек на страницу, ## 12.3.56 — исходный профиль1000 полных маршрутов (2026-10-04) Полный валидный servers DTO21 устройство/4261 bytes:WAN10×JH10×Exit10 создаёт1000 full routes.1 browser/test lint passed; раскрытый список1000 rows/33049 table DOM/33780 page DOM/3738ms dev ready. [Профиль](ux-ui/acceptance/optimizer-routes-baseline-2026-10-04.json) сохранён. Это baseline, не performance acceptance; dev latency не production benchmark. Продуктовый код не менялся, build повторно не нужен. Далее12.3.57 ограничить DOM раскрытого списка50/page без потери global sort/индекса/best/refresh/clamp; затем WAN matrix/CPU combinatorics/24 commRecs и общая регрессия. Внешняя12.5 отдельно. Локальный коммит без push. + + +## 12.3.57 — страницы полных маршрутов оптимизатора (2026-10-04) + +ReUI client pagination50 только раскрытого списка, autoResetPageIndex=false/clamp, fixed widths/ScrollArea, закреплённый исходный номер маршрута112px. _index/bestId вычислены по полному исходному списку, не странице/sort. Сворачивание/раскрытие явно reset page0; для1000 текст «Открыть все1000 комбинаций» не обещает одновременный DOM. Для<=50 прежний текст сохранён. Toggle44px. Расчёт модели/сети и полный массив не изменены. + +6/6 профильных table/pin/empty/unknown cases и финальный large1/1 passed. Первые ошибочные expectations теста считали min index уникальным; исправлены по существующим равным значениям. Проверены50 initial/счётчик номера51/refresh/page20 951–1000, collapse5/open50, глобальный min sort включает номера за исходной page1 при ties,320px/pinning/axe/shrink1000→1/no writes.21 devices/WAN10×JH10×Exit10/4261 bytes →50 rows/1699 table DOM/2461 page DOM/1853ms dev ready вместо33049/33780. TypeScript без incremental/ESLint grid+test/production build passed. [Профиль](ux-ui/acceptance/optimizer-routes-pages-2026-10-04.json), [320px](ux-ui/screens/optimizer-routes-pages-320.png) сохранены и просмотрены. Dev latency не production benchmark. + +Осталось7 manualPagination:true specialized grids. Следующий12.3.58 — WAN matrix/динамические JH columns/CPU combinatorics. Срез commRecs24/восстановление вкладки карточки/native device options1000 и полный массив в памяти остаются. Общая main/context после всех delta и внешняя12.5 не закрыты. Локальный коммит без push. diff --git a/docs/ux-ui/acceptance/optimizer-routes-pages-2026-10-04.json b/docs/ux-ui/acceptance/optimizer-routes-pages-2026-10-04.json new file mode 100644 index 00000000..b8ec4122 --- /dev/null +++ b/docs/ux-ui/acceptance/optimizer-routes-pages-2026-10-04.json @@ -0,0 +1,9 @@ +{ + "combinations": 1000, + "servers": 21, + "bytes": 4261, + "rendered": 50, + "tableNodes": 1699, + "pageNodes": 2461, + "readyMs": 1853 +} \ No newline at end of file diff --git a/docs/ux-ui/screens/optimizer-routes-pages-320.png b/docs/ux-ui/screens/optimizer-routes-pages-320.png new file mode 100644 index 0000000000000000000000000000000000000000..0430c90c7516105cde1cb559211d3212c05bf628 GIT binary patch literal 54911 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/large-optimizer-routes.spec.ts b/tests/e2e/large-optimizer-routes.spec.ts index 3c296490..90683175 100644 --- a/tests/e2e/large-optimizer-routes.spec.ts +++ b/tests/e2e/large-optimizer-routes.spec.ts @@ -1,26 +1,63 @@ import { test, expect } from "@playwright/test" import { writeFileSync } from "node:fs" +import axe from "axe-core" import { optimizerServersSchema } from "../../lib/route-optimizer-response" test("Оптимизатор1000 маршрутов: WAN10 × JH10 × Exit10", async ({ page }, info) => { test.setTimeout(90000) const common = { host: "192.0.2.1", enabled: true, status: "online", latency: 10, country: "RU", site: "TEST", model: "CHR" } - const servers = optimizerServersSchema.parse([ + let servers = optimizerServersSchema.parse([ { ...common, id: 1, name: "Домашний роутер", type: "home-router", wanUplinks: Array.from({ length: 10 }, (_, i) => ({ id: `wan${i+1}`, name: `WAN${i+1}`, isp: "ISP", iface: `ether${i+1}`, ip: `198.51.100.${i+1}`, maxDl: 100, maxUl: 100 })) }, ...Array.from({ length: 10 }, (_, i) => ({ ...common, id: i+2, name: `Jump ${i+1}`, type: "jump-host" })), ...Array.from({ length: 10 }, (_, i) => ({ ...common, id: i+12, name: `Exit ${i+1}`, type: "exit-node" })), ]) + let reads = 0 + const writes: string[] = [] await page.route("**/health", r => r.fulfill({ json: { ok: true } })) await page.route("**/api/**", r => { const path = new URL(r.request().url()).pathname + if (path === "/api/servers") reads++ + if (r.request().method() !== "GET" && !path.endsWith("/preview")) writes.push(path) return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes: [] } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/ospf/optimize/preview") ? { serverId: 1, serverName: "Домашний роутер", pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} }) }) const start = Date.now(); await page.goto("/route-optimizer?server=1&keep=1") await page.getByRole("button", { name: "Маршруты (1000)", exact: true }).click() const table = page.getByRole("table").filter({ hasText: "# Маршрут" }) await expect(table.locator("tr[data-row-id]")).toHaveCount(5) - await page.getByRole("button", { name: "Показать все 1000 комбинаций", exact: true }).click() - await expect(table.locator("tr[data-row-id]")).toHaveCount(1000) + await page.getByRole("button", { name: "Открыть все 1000 комбинаций", exact: true }).click() + await expect(table.locator("tr[data-row-id]")).toHaveCount(50) const profile = { combinations: 1000, servers: servers.length, bytes: Buffer.byteLength(JSON.stringify(servers)), rendered: await table.locator("tr[data-row-id]").count(), tableNodes: await table.locator("*").count(), pageNodes: await page.locator("*").count(), readyMs: Date.now()-start } console.log("Optimizer full routes", JSON.stringify(profile)); writeFileSync(info.outputPath("optimizer-routes-profile.json"), JSON.stringify(profile, null, 2)) + const root = page.getByRole("region", { name: "Полные маршруты оптимизатора" }) + const rows = table.locator("tr[data-row-id]") + const next = root.getByRole("button", { name: "Следующая страница", exact: true }) + await next.click(); const previous = await rows.first().getAttribute("data-row-id") + await expect(rows.first().getByRole("cell").first()).toContainText("51") + const before = reads; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => reads).toBeGreaterThan(before); await expect(rows.first()).toHaveAttribute("data-row-id", previous!) + for (let i=1; i<19; i++) await next.click() + await expect(next).toBeDisabled(); await expect(rows.first().getByRole("cell").first()).toContainText("951") + const minimum = await rows.last().getByRole("cell").nth(6).innerText() + await expect(rows.last().getByRole("cell").first()).toContainText("1000") + await root.getByRole("button", { name: "Свернуть", exact: true }).click(); await expect(rows).toHaveCount(5) + await root.getByRole("button", { name: "Открыть все 1000 комбинаций", exact: true }).click(); await expect(rows).toHaveCount(50) + await table.getByRole("button", { name: "Индекс", exact: true }).click() + await table.getByRole("button", { name: "Индекс", exact: true }).click() + await expect(rows.first().getByRole("cell").nth(6)).toHaveText(minimum) + expect(Number(await rows.last().getByRole("cell").first().innerText())).toBeGreaterThan(50) + await page.setViewportSize({ width: 320, height: 1000 }) + const scroll = root.locator('[data-slot="scroll-area-viewport"]') + await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) + expect(await rows.first().getByRole("cell").first().evaluate(el => { const a=el.getBoundingClientRect(), v=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return a.left>=v.left-1 && a.right<=v.right+1 })).toBe(true) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320) + await scroll.evaluate(el => { el.scrollLeft = 0 }) + await root.evaluate(el => el.scrollIntoView({ block: "start" })) + await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) + await page.screenshot({ path: info.outputPath("optimizer-routes-pages-320.png"), animations: "disabled" }) + await page.addScriptTag({ content: axe.source }) + expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[aria-label="Полные маршруты оптимизатора"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([]) + await next.click(); servers = servers.filter(server => server.id === 1 || server.id === 2 || server.id === 12).map(server => server.id === 1 ? { ...server, wanUplinks: server.wanUplinks!.slice(0, 1) } : server) + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(rows).toHaveCount(1); await expect(rows.first().getByRole("cell").first()).toContainText("1") + expect(writes).toEqual([]) })