From 086f118204b0f021094889434cbca68230373c88 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 17:32:01 +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=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B5=D0=B9=20OSPF=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D0=BF=D0=BE=D1=82=D0=B5=D1=80=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/ospf/page.tsx | 15 +++--- components/ospf-detail-list.tsx | 33 ++++++++++++ docs/ux-ui-performance.md | 2 + docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 9 ++++ .../acceptance/ospf-detail-2026-10-04.json | 8 +++ docs/ux-ui/screens/ospf-large-detail-320.png | Bin 0 -> 44581 bytes tests/e2e/large-ospf-neighbors.spec.ts | 50 ++++++++++++++++++ 9 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 components/ospf-detail-list.tsx create mode 100644 docs/ux-ui/acceptance/ospf-detail-2026-10-04.json create mode 100644 docs/ux-ui/screens/ospf-large-detail-320.png diff --git a/app/(main)/ospf/page.tsx b/app/(main)/ospf/page.tsx index b688ad0c..5066b480 100644 --- a/app/(main)/ospf/page.tsx +++ b/app/(main)/ospf/page.tsx @@ -1,5 +1,6 @@ "use client" +import { OspfDetailList } from "@/components/ospf-detail-list" import { OspfTopologyScope } from "@/components/ospf-topology-scope" import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion" import { deriveOspfRouterIdentity } from "@/lib/ospf-router-identity" @@ -559,7 +560,7 @@ function NodeDetailPanel({

{node.label}

Router-ID: {routerId}

- @@ -578,8 +579,7 @@ function NodeDetailPanel({

Соседи ({adjacencies.length})

{adjacencies.length === 0 &&

Нет активных adjacency

} -
- {adjacencies.map((adj, i) => { + `${adj.peerLabel} ${adj.peerId} ${adj.iface} ${adj.state}`} renderItem={(adj, i) => { const ec = EDGE_COLOR[adj.state] ?? "var(--muted-foreground)" return (
@@ -592,8 +592,7 @@ function NodeDetailPanel({ {adj.state}
) - })} -
+ }} />
@@ -601,8 +600,7 @@ function NodeDetailPanel({ Интерфейсы OSPF{ifaces.length > 0 && ({ifaces.length})}

{ifaces.length === 0 &&

Нет данных

} -
- {ifaces.map(iface => ( + `${iface.interfaceName} ${iface.area} ${iface.cost ?? ""}`} renderItem={iface => (
{iface.interfaceName} @@ -611,8 +609,7 @@ function NodeDetailPanel({ cost {iface.cost ?? "—"}
- ))} -
+ )} />
) diff --git a/components/ospf-detail-list.tsx b/components/ospf-detail-list.tsx new file mode 100644 index 00000000..b4c4e42a --- /dev/null +++ b/components/ospf-detail-list.tsx @@ -0,0 +1,33 @@ +"use client" + +import { useMemo, useState, type ReactNode } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +export function OspfDetailList({ items, label, getSearchValue, renderItem }: { + items: T[]; label: string; getSearchValue: (item: T) => string + renderItem: (item: T, index: number) => ReactNode +}) { + const [query, setQuery] = useState("") + const [page, setPage] = useState(0) + const filtered = useMemo(() => { + const value = query.trim().toLowerCase() + return value ? items.filter(item => getSearchValue(item).toLowerCase().includes(value)) : items + }, [items, query, getSearchValue]) + const pages = Math.max(1, Math.ceil(filtered.length / 50)) + if (page >= pages) setPage(pages - 1) + const current = Math.min(page, pages - 1) + const controlled = items.length > 50 || !!query + return
+ {controlled &&
+ { setQuery(event.target.value); setPage(0) }} /> +

Показаны {filtered.length ? current * 50 + 1 : 0}–{Math.min((current + 1) * 50, filtered.length)} из {filtered.length}. Всего {items.length}.

+
+ + +
+ {!filtered.length &&

По вашему запросу ничего не найдено.

} +
} +
{filtered.slice(current * 50, (current + 1) * 50).map((item, index) => renderItem(item, current * 50 + index))}
+
+} diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 2884cea1..cba9a804 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -96,3 +96,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.49: OSPF1000 neighbors→50 rows,1001 SVG nodes/12015 descendants/13597 page nodes/2827ms dev ready. URL page20/search/global Cost sort/refresh/cache/clamp/320px/axe приняты;22 browser/type/lint/build. Весь граф не принят, KPI1000 переносится на320px, большой node detail вне scope; далее12.3.50–51.9 manualPagination:true остаются. 12.3.50: OSPF1000 neighbors → scope8 связей/9 узлов, выбранный remote1000:2 SVG nodes/27 descendants/1630 page DOM.26+3 main/8 context/type/lint/build. Полный источник и статус/поиск/URL сохранены; большие node details и читаемость parallel edges не приняты. Далее12.3.51. + +12.3.51: OSPF node=1/1000 neighbors+1000 interfaces →50+50 rows/635 panel nodes/2335 page DOM.4+15+1 browser/type/lint/build. Search/full counts/refresh/clamp/320px/axe приняты; parallel link labels далее12.3.52. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 153b6e5d..bb8040e6 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-04: 12.3.50 принят локально: OSPF topology показывает до8 связей/9 узлов, полный статус и поиск/URL.26+3 main/8 context, types/lint/build; полный источник1000 сохранён. Далее12.3.51 — большие детали узла; затем читаемость параллельных связей и optimizer. Общая регрессия после delta и внешняя12.5 остаются. +Актуальное продолжение 2026-10-04: 12.3.51 принят локально: большие детали OSPF по50 neighbors/interfaces, поиск полного списка/refresh/clamp/320px/axe.4+15+1 browser/type/lint/build. Далее12.3.52 — подписи параллельных связей; затем optimizer и общая main/context после delta. Внешняя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 5e2adf8c..faafb8bc 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-04: 12.3.50 принят локально: OSPF topology показывает до8 связей/9 узлов, полный статус и поиск/URL.26+3 main/8 context, types/lint/build; полный источник1000 сохранён. Далее12.3.51 — большие детали узла; затем читаемость параллельных связей и optimizer. Общая регрессия после delta и внешняя12.5 остаются. +Актуальное продолжение 2026-10-04: 12.3.51 принят локально: большие детали OSPF по50 neighbors/interfaces, поиск полного списка/refresh/clamp/320px/axe.4+15+1 browser/type/lint/build. Далее12.3.52 — подписи параллельных связей; затем optimizer и общая main/context после delta. Внешняя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 c2b31050..70659f3c 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -603,3 +603,12 @@ OspfTopologyScope использует полный источник: до8 inci 26/26 профильных main,8/8 context и финальные3/3 large прошли. Fixture1000 neighbors/1000 interfaces/1001 nodes/583875 bytes →50 rows/2 SVG nodes/27 descendants/1630 page nodes/1531ms dev ready для URL remote1000. Проверены page2/clamp после уменьшения1000→13→12, полная hidden Full-связь при первых Down, выбор remote1000 из всего каталога/keep, close/focus,320px/axe, bounded parallel1000. TypeScript без incremental, ESLint страницы/компонента/теста и production build прошли. [Профиль](ux-ui/acceptance/ospf-topology-scope-2026-10-04.json), [срез1440px](ux-ui/screens/ospf-topology-scope-1440.png), [320px](ux-ui/screens/ospf-topology-scope-320.png) сохранены и просмотрены. Dev latency не production benchmark. На большой топологии SVG доступен через горизонтальный scroll; все узлы доступны через поиск. Это не приёмка всех вариантов графа: параллельные edges ограничены по DOM, но их подписи на общей линии могут перекрываться. Большая NodeDetailPanel ещё рендерит все1000 neighbors/interfaces — следующий12.3.51. Затем параллельные связи/optimizer/общая main-context регрессия после delta.9 manualPagination:true specialized grids остаются. Внешние RouterOS/PG/интеграции12.5 не проверены. Локальный коммит без push. + + +## 12.3.51 — большие детали узла OSPF (2026-10-04) + +OspfDetailList: client pages50 отдельно для соседей/интерфейсов, поиск всего списка по peer/Router ID/interface/state и interface/area/cost; full counts сохраняются. Refresh не сбрасывает ручную страницу, shrink ограничивает её, новый nodeId пересоздаёт контекст списка. Close44px, Escape/focus прежние; сетевые правила не изменены. + +4/4 large,15/15 profile keyboard/layout/URL/state/identity и финальный detail1/1 прошли. Полный fixture1000 neighbors/1000 interfaces →50+50 записей/635 panel DOM/2335 page DOM для URL node=1. Поиск ether1000/полный cost1000, no-match, page2/refresh, shrink1000→1/keep/node,320px/кнопки44/axe проверены. TypeScript без incremental, полный ESLint страницы/компонента/теста и production build прошли. [Профиль](ux-ui/acceptance/ospf-detail-2026-10-04.json), [панель320px](ux-ui/screens/ospf-large-detail-320.png) сохранены и просмотрены. Каталог не обрезается в памяти. + +Следующий12.3.52: визуальная группировка параллельных links без подмены cost/state; затем optimizer и общая регрессия после всех delta.9 specialized manualPagination:true остаются. Внешние RouterOS/PG/интеграции12.5 не проверены. Локальный коммит без push. diff --git a/docs/ux-ui/acceptance/ospf-detail-2026-10-04.json b/docs/ux-ui/acceptance/ospf-detail-2026-10-04.json new file mode 100644 index 00000000..a38dbc6f --- /dev/null +++ b/docs/ux-ui/acceptance/ospf-detail-2026-10-04.json @@ -0,0 +1,8 @@ +{ + "neighbors": 1000, + "interfaces": 1000, + "renderedNeighbors": 50, + "renderedInterfaces": 50, + "panelNodes": 635, + "pageNodes": 2335 +} \ No newline at end of file diff --git a/docs/ux-ui/screens/ospf-large-detail-320.png b/docs/ux-ui/screens/ospf-large-detail-320.png new file mode 100644 index 0000000000000000000000000000000000000000..5ac1dbedb7e52003cae624f7dace78614a52369d GIT binary patch literal 44581 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
3ChlZrbZK`4sazb;*_KRrce?c#?@5r2qQ8 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%0D%w!k3t?LGq8jvQa$C!ibRa$=bx HI-dUrNCb_> literal 0 HcmV?d00001 diff --git a/tests/e2e/large-ospf-neighbors.spec.ts b/tests/e2e/large-ospf-neighbors.spec.ts index e99ca376..3e944b73 100644 --- a/tests/e2e/large-ospf-neighbors.spec.ts +++ b/tests/e2e/large-ospf-neighbors.spec.ts @@ -144,3 +144,53 @@ test("OSPF: тысяча параллельных связей не обходи await expect(page.locator("[data-ospf-node]")).toHaveCount(2) expect(await page.getByRole("group", { name: "Топология OSPF", exact: true }).locator("*").count()).toBeLessThan(200) }) + + +test("OSPF детали1000: полные списки, поиск, страницы и сохранение узла", async ({ page }, info) => { + test.setTimeout(90000) + const host = { serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU" } + let neighbors = Array.from({ length: 1000 }, (_, i) => ({ ...host, id: `n${i + 1}`, address: `10.${Math.floor(i / 256)}.${i % 256}.1`, routerId: `10.${Math.floor(i / 256)}.${i % 256}.1`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: `ether${i + 1}`, state: "Full", uptime: "1h", stateChanges: 0, priority: 1 })) + const interfaces = neighbors.map((n, i) => ({ ...host, id: `i${i + 1}`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: n.interface, cost: i + 1, type: "broadcast", disabled: false, inactive: false, priority: 1, helloInterval: 10, deadInterval: 40, useBfd: false })) + const catalog = ospfCatalogSchema.parse({ neighbors, interfaces, instances: [{ ...host, id: "instance1", name: "default", routerId: "192.0.2.1", version: 2, disabled: false, inactive: false, redistribute: "" }], bfdSessions: [], routes: [], failures: [] }) + + let currentInterfaces = interfaces + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + const path = new URL(r.request().url()).pathname + return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/ospf/catalog" ? { ...catalog, neighbors, interfaces: currentInterfaces } : path === "/api/servers" ? [{ id: 1, name: "Роутер 1", host: "192.0.2.1", enabled: true, country: "RU", status: null }] : {} }) + }) + await page.goto("/ospf?tab=neighbors&server=1&node=1&keep=1") + const details = page.getByRole("region", { name: "Детали узла OSPF" }) + const peers = details.getByRole("region", { name: "Соседи узла OSPF", exact: true }) + const ports = details.getByRole("region", { name: "Интерфейсы узла OSPF", exact: true }) + const peerRows = peers.locator(':scope > div').last().locator(':scope > div') + const portRows = ports.locator(':scope > div').last().locator(':scope > div') + await expect(peerRows).toHaveCount(50); await expect(portRows).toHaveCount(50) + await expect(peers.getByRole("status")).toContainText("1–50 из 1000. Всего 1000") + const profile = { neighbors: neighbors.length, interfaces: interfaces.length, renderedNeighbors: await peerRows.count(), renderedInterfaces: await portRows.count(), panelNodes: await details.locator("*").count(), pageNodes: await page.locator("*").count() } + console.log("OSPF detail", JSON.stringify(profile)); writeFileSync(info.outputPath("ospf-detail-profile.json"), JSON.stringify(profile, null, 2)) + expect(profile.pageNodes).toBeLessThan(5000) + await peers.getByRole("button", { name: "Следующая страница: Соседи узла OSPF", exact: true }).click() + await expect(peerRows.first()).toContainText("ether51") + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(peers.getByRole("status")).toContainText("51–100 из 1000") + const peerSearch = peers.getByRole("textbox", { name: "Поиск: Соседи узла OSPF" }) + await peerSearch.fill("ether1000"); await expect(peerRows).toHaveCount(1); await expect(peerRows.first()).toContainText("10.3.231.1") + const portSearch = ports.getByRole("textbox", { name: "Поиск: Интерфейсы узла OSPF" }) + await portSearch.fill("ether1000"); await expect(portRows).toHaveCount(1); await expect(portRows.first()).toContainText("cost 1000") + await portSearch.fill("несуществующий"); await expect(ports).toContainText("По вашему запросу ничего не найдено") + await peerSearch.fill(""); await portSearch.fill("") + await page.setViewportSize({ width: 320, height: 1000 }); await details.evaluate(el => el.scrollIntoView({ block: "start" })) + for (const button of await details.locator("button").all()) expect((await button.boundingBox())!.height).toBeGreaterThanOrEqual(44) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320) + await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) + await page.screenshot({ path: info.outputPath("ospf-large-detail-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="Детали узла OSPF"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([]) + await peers.getByRole("button", { name: "Следующая страница: Соседи узла OSPF", exact: true }).click() + neighbors = neighbors.slice(0, 1); currentInterfaces = interfaces.slice(0, 1) + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(peerRows).toHaveCount(1); await expect(portRows).toHaveCount(1) + await expect(peerRows.first()).toContainText("ether1") + await expect(page).toHaveURL(url => url.searchParams.get("node") === "1" && url.searchParams.get("keep") === "1") +})