diff --git a/components/data-grids/bgp-sessions-data-grid.tsx b/components/data-grids/bgp-sessions-data-grid.tsx index f0504675..4d82130d 100644 --- a/components/data-grids/bgp-sessions-data-grid.tsx +++ b/components/data-grids/bgp-sessions-data-grid.tsx @@ -2,7 +2,7 @@ import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo } from "react" +import { useMemo, useEffect } from "react" import { type ColumnDef, useTable, @@ -266,7 +266,9 @@ function BgpSessionsDataGrid({ sessions, emptyTitle, selectedSessionId, onSelect const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 } }, data: sessions, columns, getRowId: (row) => row.id, @@ -274,6 +276,18 @@ function BgpSessionsDataGrid({ sessions, emptyTitle, selectedSessionId, onSelect ...(onSelectSession ? { state: { expanded: selectedSessionId ? { [selectedSessionId]: true } : {} } } : {}), }) + const {pageIndex,pageSize}=table.state.pagination + const sorting=table.state.sorting + useEffect(()=>{ + const last=Math.max(0,Math.ceil(sessions.length/pageSize)-1) + if(pageIndex>last)table.setPageIndex(last) + },[sessions.length,pageSize,pageIndex,table]) + useEffect(()=>{ + if(!selectedSessionId)return + const index=table.getPrePaginatedRowModel().rows.findIndex(row=>row.id===selectedSessionId) + if(index>=0)table.setPageIndex(Math.floor(index/pageSize)) + },[selectedSessionId,pageSize,sessions.length,sorting,table]) + if (sessions.length === 0) { return ( 50 || pageSize!==50} onRowClick={(row) => onSelectSession ? onSelectSession(row.id === selectedSessionId ? null : row.id) : table.getRow(row.id).toggleExpanded()} /> ) diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index f549a897..ad7890cd 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-03 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. Следующий блок12.3.4: большой BGP каталог/сохранение страницы при обновлении; затем остальные специализированные grids и12.4 регрессия/build. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Далее production build накопленных изменений и переходы дашборда, затем12.3.5 остальные специализированные grids и12.4 сводная регрессия. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ca273f49..d05ce609 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,27 @@ +## 12.3.4 — большой каталог BGP и контекст страницы (2026-10-04) + +BGP использует штатную client pagination50. При обновлении того же каталога страница сохраняется (autoResetPageIndex:false); при уменьшении результатов индекс ограничивается последней существующей страницей. Сессия из URL открывает соответствующую страницу по pre-paginated row model; изменение сортировки/page size пересчитывает её положение. Client фильтры/сортировка и раскрытие работают по всем данным, права/сетевые операции неизменны. + +На фиксированных1000 сессиях DOM25657→1932 nodes, rendered1000→50. Единичные dev ready до2990ms/после898ms не являются production SLA.13 BGP/routing/link/large browser, typecheck/ESLint прошли, включая обновление на странице2, поиск последней сессии, URL последней страницы/детали и320px. ReUI DataGridPagination повторно использован; upstream API и установленная групповая навигация различаются, primitive не менялся. + +| Доработка | Референс | Схема | +| --- | --- | --- | +| Серверы/CompactDataGrid/BGP | [Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Полный каталог → поиск/сортировка → страница50 → детали | +| Большая очередь проблем | [List9](https://reui.io/preview/base/list-9?ref=mcp) | Полный счётчик → первые5 → раскрыть50 → добавить50 → объект | + +```mermaid +flowchart LR + A["1000 строк API"] --> B["Проверка DTO и фильтры"] + B --> C["ReUI/TanStack: 50 строк в DOM"] + C --> D["Страница и детали сессии"] + D --> E["Обновление сохраняет страницу"] +``` + +Этап12.3 ещё в работе: остальные27 manualPagination grids/широкие pivot не проверены на1000 строк. Следующий шаг: production build накопленных изменений, профильная регрессия переходов дашборда; затем12.3.5 остальные grids, отдельно Firewall порядок/drag и server-paged traffic без слепой client миграции. Итоговая12.4 и внешняя12.5 не приняты. + ## 12.3.3 — DOM очереди проблем дашборда (2026-10-04) AttentionRows больше не создаёт весь скрытый список: первые5 всегда видны, native details монтирует следующие50 по запросу, кнопка добавляет по50. Закрытие удаляет скрытые строки и сбрасывает порцию; полный счётчик/ссылки сохраняются. Нет скрытых1000 anchors до действия пользователя. Раскрытие выполняется обычным summary и клавиатурой, используется существующий Button; Frame/AttentionQueue primitives unchanged. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index ca875e1d..0114e4cb 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -126,3 +126,24 @@ AttentionRows больше не создаёт весь скрытый спис Проверки:62 dashboard/permissions browser (включая1000 проблем: DOM5→55→105→5→55, переход к серверу,320px),20 dashboard unit, typecheck/ESLint прошли. На fixture1000 offline servers начальный dashboard:831 DOM nodes,1081ms ready в одном dev прогоне. Порционное раскрытие может по запросу пользователя постепенно создать весь список; полноценная виртуализация очереди не заявляется. Далее12.3.4: большой каталог BGP, учитывая сохранение страницы при фоновом обновлении. Другие27 специализированных manualPagination grids и wide pivot остаются отдельными ограничениями.12.4 сводные проверки/production build ещё не приняты; внешняя приёмка не заменяется локальными данными. + +## 12.3.4 — большой каталог BGP и контекст страницы (2026-10-04) + +BGP использует штатную client pagination50. При обновлении того же каталога страница сохраняется (autoResetPageIndex:false); при уменьшении результатов индекс ограничивается последней существующей страницей. Сессия из URL открывает соответствующую страницу по pre-paginated row model; изменение сортировки/page size пересчитывает её положение. Client фильтры/сортировка и раскрытие работают по всем данным, права/сетевые операции неизменны. + +На фиксированных1000 сессиях DOM25657→1932 nodes, rendered1000→50. Единичные dev ready до2990ms/после898ms не являются production SLA.13 BGP/routing/link/large browser, typecheck/ESLint прошли, включая обновление на странице2, поиск последней сессии, URL последней страницы/детали и320px. ReUI DataGridPagination повторно использован; upstream API и установленная групповая навигация различаются, primitive не менялся. + +| Доработка | Референс | Схема | +| --- | --- | --- | +| Серверы/CompactDataGrid/BGP | [Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Полный каталог → поиск/сортировка → страница50 → детали | +| Большая очередь проблем | [List9](https://reui.io/preview/base/list-9?ref=mcp) | Полный счётчик → первые5 → раскрыть50 → добавить50 → объект | + +```mermaid +flowchart LR + A["1000 строк API"] --> B["Проверка DTO и фильтры"] + B --> C["ReUI/TanStack: 50 строк в DOM"] + C --> D["Страница и детали сессии"] + D --> E["Обновление сохраняет страницу"] +``` + +Этап12.3 ещё в работе: остальные27 manualPagination grids/широкие pivot не проверены на1000 строк. Следующий шаг: production build накопленных изменений, профильная регрессия переходов дашборда; затем12.3.5 остальные grids, отдельно Firewall порядок/drag и server-paged traffic без слепой client миграции. Итоговая12.4 и внешняя12.5 не приняты. diff --git a/tests/e2e/large-bgp.spec.ts b/tests/e2e/large-bgp.spec.ts new file mode 100644 index 00000000..7a7bb001 --- /dev/null +++ b/tests/e2e/large-bgp.spec.ts @@ -0,0 +1,8 @@ +import {test,expect} from "@playwright/test" +import {bgpSession} from "./fixtures/bgp" +test("large BGP bounds DOM, preserves page on refresh and restores linked session",async({page},info)=>{ + test.setTimeout(90000);const sessions=Array.from({length:1000},(_,i)=>({...bgpSession(1,i),id:"*"+(i+1),name:"Large peer "+String(i+1).padStart(4,"0"),remoteAs:65000+i,capabilities:["route-refresh"]}));let calls=0 + 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/bgp/catalog")calls++;return r.fulfill({json:path==="/api/auth/config"?{required:false}:path==="/api/servers"?[{id:1,name:"BGP node",host:"192.0.2.1",country:"RU",enabled:true,status:"online"}]:path==="/api/bgp/catalog"?{sessions,failures:[]}:{}})}) + const start=Date.now();await page.goto("/bgp?server=1");await expect(page.getByText("Large peer 0001",{exact:true})).toBeVisible({timeout:60000});const sample={fixtureRows:1000,readyMs:Date.now()-start,renderedRows:await page.locator("tr[data-row-id]").count(),domNodes:await page.locator("*").count()};console.log(JSON.stringify(sample));await info.attach("large-bgp-metrics",{body:JSON.stringify(sample),contentType:"application/json"});expect(sample.renderedRows).toBe(50);expect(sample.domNodes).toBeLessThan(3500) + await page.getByRole("button",{name:"Следующая страница",exact:true}).click();await expect(page.getByText("Large peer 0051",{exact:true})).toBeVisible();const before=calls;await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect.poll(()=>calls).toBeGreaterThan(before);await expect(page.getByRole("button",{name:"Обновить",exact:true})).toBeEnabled();await expect(page.getByText("51–100 из 1000",{exact:true})).toBeVisible();await page.getByPlaceholder("IP, AS, описание…").fill("Large peer 1000");await expect(page.locator("tr[data-row-id]")).toHaveCount(1);await expect(page.getByText("Large peer 1000",{exact:true})).toBeVisible();await page.goto("/bgp?server=1&session=1-%2A1000");await expect(page.getByText("Large peer 1000",{exact:true})).toBeVisible();await expect(page.getByText("route-refresh",{exact:true})).toBeVisible();await expect(page.getByText("951–1000 из 1000",{exact:true})).toBeVisible();await page.setViewportSize({width:320,height:900});expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true) +})