From 991f2b38711fa07cc8d3c6e82fa55f731b3aa698 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 22:15:00 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=83=D1=87=D0=B8=D1=82=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=D0=B5=D1=81=D1=8C=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=20Community=20=D0=B2=20=D0=BE?= =?UTF-8?q?=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route-optimizer-comm-recs-data-grid.tsx | 46 +++++++++++++++--- docs/ux-ui-performance.md | 9 ++++ docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 9 ++++ .../ux-ui/screens/optimizer-community-320.png | Bin 0 -> 53530 bytes lib/route-optimizer-data.ts | 2 +- lib/route-optimizer-observed.test.ts | 10 ++++ tests/e2e/large-optimizer-community.spec.ts | 32 ++++++++++-- 9 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 docs/ux-ui/screens/optimizer-community-320.png diff --git a/components/data-grids/route-optimizer-comm-recs-data-grid.tsx b/components/data-grids/route-optimizer-comm-recs-data-grid.tsx index 7696cf19..cbbb147c 100644 --- a/components/data-grids/route-optimizer-comm-recs-data-grid.tsx +++ b/components/data-grids/route-optimizer-comm-recs-data-grid.tsx @@ -2,7 +2,9 @@ import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo } from "react" +import { useEffect, useMemo, useState } from "react" +import { Input } from "@/components/ui/input" +import { DataGridContainer, DataGridScrollArea, DataGridTable } from "@/components/reui/data-grid" import { type ColumnDef, useTable, @@ -68,15 +70,22 @@ function RouteOptimizerCommRecsDataGrid({ onApply, simulation = false, }: RouteOptimizerCommRecsDataGridProps) { + const [query, setQuery] = useState("") const rows = useMemo( () => recs.map((r, idx) => ({ ...r, _rowKey: `${homeId}::${r.community}::${idx}` })), [homeId, recs], ) + const filtered = useMemo(() => rows.filter(row => + [row.community, row.communityName, row.current?.wan, row.current?.jh, row.current?.exit, row.current?.gateway, + row.recommended?.wan, row.recommended?.jh, row.recommended?.exit, row.recommended?.gateway] + .join(" ").toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), [rows, query]) + const columns = useMemo[]>( () => [ { id: "community", + size: 180, accessorKey: "community", header: ({ column }) => ( @@ -95,6 +104,7 @@ function RouteOptimizerCommRecsDataGrid({ }, { id: "current", + size: 260, header: () => ( Базовый расчёт (WAN → JH → Exit) @@ -123,6 +133,7 @@ function RouteOptimizerCommRecsDataGrid({ }, { id: "recommended", + size: 260, header: () => Рекомендуемый, enableSorting: false, cell: ({ row }) => { @@ -154,6 +165,7 @@ function RouteOptimizerCommRecsDataGrid({ }, { id: "probability", + size: 160, header: () => ( Индекс (баз / рек) @@ -178,6 +190,7 @@ function RouteOptimizerCommRecsDataGrid({ }, { id: "actions", + size: 240, header: () => ( Действие @@ -236,16 +249,32 @@ function RouteOptimizerCommRecsDataGrid({ const table = useTable({ features: dataGridFeatures, - manualPagination: true, - data: rows, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["community"], end: [] } }, + data: filtered, columns, getRowId: (row) => row._rowKey, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(filtered.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [filtered.length, pageIndex, pageSize, table]) + return ( +
+
+ +

Community: {filtered.length} из {rows.length}. Индексы и пути — расчётная модель.

+
50 || pageSize !== 50} tableClassNames={{ headerRow: "border-b bg-muted/20", bodyRow: cn( @@ -254,8 +283,13 @@ function RouteOptimizerCommRecsDataGrid({ "[&:has([data-comm-applied=true])]:bg-success/5", ), }} - tableLayout={{ dense: true }} - /> + tableLayout={{ dense: true, width: "fixed", columnsPinnable: true }} + > + + + + +
) } diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index d3dcaca9..cfc1ca28 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -147,3 +147,12 @@ WAN1000×JH1:1000→50rows,28019→1419tableDOM,28712→2149pageDOM,2264ms dev-r ## 12.3.62 — исходный скрытый срез Community 2026-10-04. Live API fixture1000 правил Home1/1WAN/1JH/безExit, strict servers/rules DTO. UI показывает BGP community(24),24rows/504tableDOM/1197pageDOM/1916ms dev-ready. Это не performance-успех:976 правил вообще исключены до grid, counts не показывают реальный источник.1 browser/test lint passed, API writes отсутствуют. Следующий12.3.63 удаляет срез и ограничивает отображение pages50, сохраняя unknown indices безExit. + + +## 12.3.63 — весь Community-каталог с ограниченным отображением + +2026-10-04. Удалён скрытый slice24 после фильтра sourceServerId; ownership и расчётная семантика сохранены. ReUI DataGrid pages50/full search/communityName/gateway/path, exact counts, full-model sort, fixed widths/horizontalScroll/pinnedCommunity. Row key формируется до фильтрации из прежнего home/community/index; pins сохраняются при search/sort/страницах/refresh. Global рекомендаций count теперь относится ко всем правилам. Live режим по-прежнему не применяет BGP-рекомендации; симуляция доступна только demo. + +1000source→50 rendered/1024tableDOM/1751pageDOM/1851ms dev-ready; раньше24 rendered соответствовали24 в модели,976 правил терялись. Проверены page20 и правило1000/search/unknown indices безExit/no simulation buttons/refresh/clamp1/320px/pinned column/axe.7 main browser,8 pure tests (1200 assignment fixtures и новый1000 ownership/unknown), TypeScript, targeted lint и production build прошли. Снимок docs/ux-ui/screens/optimizer-community-320.png просмотрен. + +Осталось5manualPagination:true: scheduler11 fixedkeys, live GRE pools пусты/demo ограничен fixtures, probes schedule толькоdemo, subusers не подключён, trafficflows серверныйtop50 принят с честным указанием среза. Не мигрировать искусственно недостижимые live каталоги. Дальше12.4.6/12.4.7 — повтор общей main/context матрицы после delta. Внешняя12.5 не выполнена. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 96d9d0d3..4be8d604 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-04: 12.3.62 baseline Community1000 сохранён: UI учитывает24 из1000. Следующий12.3.63: убрать скрытый срез, pages50/поиск по полному каталогу/sort/pin/cache/clamp/320px. Общая регрессия после delta и внешняя12.5 остаются. +Актуальное продолжение 2026-10-04: 12.3.63 завершён: полный Community-каталог без скрытого среза24, pages50/search/sort/pin/cache/clamp/320px/axe. Следующий12.4.6: общий main прогон текущего HEAD, затем12.4.7 context. Внешняя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 a08468bb..b02dde56 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-04: 12.3.62 baseline Community1000 сохранён: UI учитывает24 из1000. Следующий12.3.63: убрать скрытый срез, pages50/поиск по полному каталогу/sort/pin/cache/clamp/320px. Общая регрессия после delta и внешняя12.5 остаются. +Актуальное продолжение 2026-10-04: 12.3.63 завершён: полный Community-каталог без скрытого среза24, pages50/search/sort/pin/cache/clamp/320px/axe. Следующий12.4.6: общий main прогон текущего HEAD, затем12.4.7 context. Внешняя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 331eef2b..32c3e67a 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -699,3 +699,12 @@ WAN1000×JH1:1000→50rows,28019→1419tableDOM,28712→2149pageDOM,2264ms dev-r ## 12.3.62 — исходный скрытый срез Community 2026-10-04. Live API fixture1000 правил Home1/1WAN/1JH/безExit, strict servers/rules DTO. UI показывает BGP community(24),24rows/504tableDOM/1197pageDOM/1916ms dev-ready. Это не performance-успех:976 правил вообще исключены до grid, counts не показывают реальный источник.1 browser/test lint passed, API writes отсутствуют. Следующий12.3.63 удаляет срез и ограничивает отображение pages50, сохраняя unknown indices безExit. + + +## 12.3.63 — весь Community-каталог с ограниченным отображением + +2026-10-04. Удалён скрытый slice24 после фильтра sourceServerId; ownership и расчётная семантика сохранены. ReUI DataGrid pages50/full search/communityName/gateway/path, exact counts, full-model sort, fixed widths/horizontalScroll/pinnedCommunity. Row key формируется до фильтрации из прежнего home/community/index; pins сохраняются при search/sort/страницах/refresh. Global рекомендаций count теперь относится ко всем правилам. Live режим по-прежнему не применяет BGP-рекомендации; симуляция доступна только demo. + +1000source→50 rendered/1024tableDOM/1751pageDOM/1851ms dev-ready; раньше24 rendered соответствовали24 в модели,976 правил терялись. Проверены page20 и правило1000/search/unknown indices безExit/no simulation buttons/refresh/clamp1/320px/pinned column/axe.7 main browser,8 pure tests (1200 assignment fixtures и новый1000 ownership/unknown), TypeScript, targeted lint и production build прошли. Снимок docs/ux-ui/screens/optimizer-community-320.png просмотрен. + +Осталось5manualPagination:true: scheduler11 fixedkeys, live GRE pools пусты/demo ограничен fixtures, probes schedule толькоdemo, subusers не подключён, trafficflows серверныйtop50 принят с честным указанием среза. Не мигрировать искусственно недостижимые live каталоги. Дальше12.4.6/12.4.7 — повтор общей main/context матрицы после delta. Внешняя12.5 не выполнена. diff --git a/docs/ux-ui/screens/optimizer-community-320.png b/docs/ux-ui/screens/optimizer-community-320.png new file mode 100644 index 0000000000000000000000000000000000000000..30246e81ba7bd51e15f706b4fc8f911cdf618c34 GIT binary patch literal 53530 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 u6S_Qm9rvjKfYVc b.probabilityOptimal - a.probabilityOptimal) const best = sorted[0] ?? null - const commRecs: CommRec[] = flatRules.filter(rule=>rule.sourceServerId===home.id).slice(0, 24).map((rule) => { + const commRecs: CommRec[] = flatRules.filter(rule=>rule.sourceServerId===home.id).map((rule) => { const curProb = 38 + (rule.community.length % 12) const recProb = best ? Math.min(92, curProb + 18 + (best.score % 10)) : curProb const rec = best diff --git a/lib/route-optimizer-observed.test.ts b/lib/route-optimizer-observed.test.ts index 118bbab0..f7d439fa 100644 --- a/lib/route-optimizer-observed.test.ts +++ b/lib/route-optimizer-observed.test.ts @@ -40,3 +40,13 @@ test("Community recommendations belong to their source home only",()=>{ assert.deepEqual(result.homes[0].commRecs.map(r=>r.community),["65001:1"]) assert.deepEqual(result.homes[1].commRecs.map(r=>r.community),["65001:4"]) }) + + +test("Community catalog retains all 1000 source rules without inventing indices",()=>{ + const inventory=rows.filter(row=>row.type!=="exit-node") + const rules=Array.from({length:1000},(_,i)=>({id:String(i+1),community:'65001:'+String(i+1),communityName:'Service '+(i+1),gateway:'192.0.2.2',gatewayTunnelId:'',description:''})) + const result=buildLiveOptimizerData(inventory,[{serverId:'1',rules}],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS) + const recs=result.homes[0].commRecs + assert.equal(recs.length,1000);assert.equal(recs[999].community,'65001:1000') + assert.ok(recs.every(rec=>rec.current===null&&rec.recommended===null&&!rec.shouldSwitch)) +}) diff --git a/tests/e2e/large-optimizer-community.spec.ts b/tests/e2e/large-optimizer-community.spec.ts index af144afe..b7d0ff6c 100644 --- a/tests/e2e/large-optimizer-community.spec.ts +++ b/tests/e2e/large-optimizer-community.spec.ts @@ -1,10 +1,11 @@ import { test, expect } from "@playwright/test" +import axe from "axe-core" import { writeFileSync } from "node:fs" import { optimizerServersSchema,optimizerRulesSchema } from "../../lib/route-optimizer-response" test("Оптимизатор1000 Community: полный источник",async({page},info)=>{ test.setTimeout(90000) - const rules=optimizerRulesSchema.parse({rulesets:[{serverId:"1",rules:Array.from({length:1000},(_,i)=>({id:String(i+1),community:`65001:${String(i+1).padStart(4,"0")}`,communityName:`Сервис ${i+1}`,gateway:"192.0.2.2",gatewayTunnelId:"",description:""}))}]}) + let rules=optimizerRulesSchema.parse({rulesets:[{serverId:"1",rules:Array.from({length:1000},(_,i)=>({id:String(i+1),community:`65001:${String(i+1).padStart(4,"0")}`,communityName:`Сервис ${i+1}`,gateway:"192.0.2.2",gatewayTunnelId:"",description:""}))}]}) const common = { host: "192.0.2.1", enabled: true, status: "online", latency: 10, country: "RU", site: "TEST", model: "CHR" } const servers = optimizerServersSchema.parse([ { ...common, id: 1, name: "Домашний роутер", type: "home-router", wanUplinks: Array.from({ length: 1 }, (_, 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 })) }, @@ -12,10 +13,12 @@ test("Оптимизатор1000 Community: полный источник",async ]) + 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/filters/rules")reads++ if (r.request().method() !== "GET" && !path.endsWith("/preview")) writes.push(path) @@ -24,10 +27,33 @@ test("Оптимизатор1000 Community: полный источник",async const start=Date.now();await page.goto("/route-optimizer?server=1&keep=1") - await page.getByRole("button",{name:"BGP community (24)",exact:true}).click() + await page.getByRole("button",{name:"BGP community (1000)",exact:true}).click() const table=page.getByRole("table").filter({hasText:"Community"}) - await expect(table.locator("tr[data-row-id]")).toHaveCount(24) + await expect(table.locator("tr[data-row-id]")).toHaveCount(50) const profile={sourceCount:1000,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 Community",JSON.stringify(profile));writeFileSync(info.outputPath("community-profile.json"),JSON.stringify(profile,null,2)) + const root=page.getByRole("region",{name:"Рекомендации BGP Community"}) + const rows=table.locator("tr[data-row-id]"),search=root.getByRole("textbox",{name:"Поиск Community",exact:true}) + const next=root.getByRole("button",{name:"Следующая страница",exact:true}) + await next.click();await expect(rows.first()).toContainText("65001:0051") + const before=reads;await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect.poll(()=>reads).toBeGreaterThan(before);await expect(rows.first()).toContainText("65001:0051") + await search.fill("Сервис 1000");await expect(rows).toHaveCount(1);await expect(rows.first()).toContainText("65001:1000") + await rows.first().getByRole("button",{name:"Закрепить",exact:true}).click();await expect(rows.first().getByRole("button",{name:"Открепить",exact:true})).toBeVisible() + await expect(rows.first().getByRole("cell").nth(3).getByLabel("Индекс неизвестен")).toHaveCount(2) + await expect(root.getByRole("button",{name:"Симулировать",exact:true})).toHaveCount(0) + await search.fill("no-such-community");await expect(rows).toHaveCount(0);await expect(root).toContainText("Community: 0 из 1000") + await search.fill("");for(let i=0;i<19;i++)await next.click() + await expect(next).toBeDisabled();await expect(rows.last()).toContainText("65001:1000");await expect(rows.last().getByRole("button",{name:"Открепить",exact:true})).toBeVisible() + await search.fill("65001:");await table.getByRole("button",{name:"Community",exact:true}).click();await table.getByRole("button",{name:"Community",exact:true}).click() + await expect(rows.first()).toContainText("65001:1000");await expect(rows.first().getByRole("button",{name:"Открепить",exact:true})).toBeVisible() + 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-community-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="Рекомендации BGP Community"]')!,{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();rules={rulesets:[{serverId:"1",rules:rules.rulesets[0].rules.slice(0,1)}]};await page.getByRole("button",{name:"Обновить",exact:true}).click() + await expect(page.getByRole("button",{name:"BGP community (1)",exact:true})).toBeVisible();await expect(rows).toHaveCount(1);await expect(rows.first()).toContainText("65001:0001") expect(writes).toEqual([]) })