fix(ui): улучшить мобильную сводку и детали BGP

This commit is contained in:
Denozordec committed 2026-09-30 17:23:18 +07:00
1 parent aa678b9d5d
commit 72a63b4a4f
13 files changed
+76 -15

No files matched your search

+11 -7
View File
@@ -286,6 +286,7 @@ function SessionsTab({ sessions, emptyTitle, selectedSessionId, onSelectSession
{urlError && <Alert variant="warning"><AlertCircleIcon /><AlertDescription>{urlError}<Button variant="outline" size="sm" onClick={reset}>Сбросить фильтры BGP</Button></AlertDescription></Alert>}
<DataPageCard>
<DataPageToolbar
className="[&>div:first-child]:max-w-full [&>div:first-child]:overflow-x-auto [&>div:first-child_button]:shrink-0 [&>div:first-child_button]:whitespace-nowrap"
segmented={{
value: stateFilter,
onChange: setStateFilter,
@@ -324,6 +325,7 @@ function SessionsTab({ sessions, emptyTitle, selectedSessionId, onSelectSession
</div>
}
/>
{selectedSessionId && sessions.some(s => s.id === selectedSessionId) && !filtered.some(s => s.id === selectedSessionId) && <p role="status" className="px-5 py-2 text-xs text-muted-foreground">Сессия из ссылки скрыта текущими фильтрами.</p>}
{hasFilters && !urlError && <Button variant="ghost" size="sm" onClick={reset}>Сбросить фильтры BGP</Button>}
<BgpSessionsDataGrid sessions={filtered} emptyTitle={urlError ? "Некорректные фильтры BGP" : emptyTitle} selectedSessionId={selectedSessionId} onSelectSession={onSelectSession} />
</DataPageCard>
@@ -454,18 +456,19 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
return (
<div className="flex flex-col gap-5">
<KpiStatGrid
gridClassName="grid-cols-2 @3xl:grid-cols-2 @6xl:grid-cols-4"
aria-label="Сводка префиксов BGP"
items={[
{
id: "rx",
label: "Всего префиксов",
label: "Префиксы",
value: fmtNum(totalRx),
icon: <DownloadIcon className="size-4" />,
iconClassName: "text-success",
},
{
id: "active",
label: "Активных маршрутов",
label: "Маршруты",
hint: totalActive === null ? "нет данных API" : undefined,
value: fmtNum(totalActive),
icon: <GitMergeIcon className="size-4" />,
@@ -473,14 +476,14 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
},
{
id: "ebgp",
label: "eBGP сессий",
label: "eBGP",
value: sessions.some(s => s.type === "Unknown") ? "—" : ebgpSessions,
icon: <ActivityIcon className="size-4" />,
iconClassName: "text-muted-foreground",
},
{
id: "ibgp",
label: "iBGP сессий",
label: "iBGP",
value: sessions.some(s => s.type === "Unknown") ? "—" : ibgpSessions,
icon: <ServerIcon className="size-4" />,
iconClassName: "text-muted-foreground",
@@ -809,11 +812,12 @@ function BgpPageContent({ selectedServerId, selectedSessionId, activeTab, onSele
)}
<KpiStatGrid
gridClassName="grid-cols-2 @3xl:grid-cols-2 @6xl:grid-cols-4"
aria-label="Сводка BGP"
items={[
{
id: "sessions",
label: "Сессий всего",
label: "Сессий",
value: incomplete ? "—" : sessions.length,
hint: incomplete ? "неизвестно" : undefined,
icon: <GitMergeIcon className="size-4" />,
@@ -828,7 +832,7 @@ function BgpPageContent({ selectedServerId, selectedSessionId, activeTab, onSele
},
{
id: "not-estab",
label: "Не установлено",
label: "Не поднято",
value: incomplete || sessions.some(s => s.state === "Unknown") ? "—" : notEstab,
icon: <AlertCircleIcon className="size-4" />,
iconClassName: notEstab > 0 ? "text-warning" : "text-muted-foreground",
@@ -836,7 +840,7 @@ function BgpPageContent({ selectedServerId, selectedSessionId, activeTab, onSele
},
{
id: "prefixes",
label: "Получено префиксов",
label: "Префиксы",
value: incomplete ? "—" : fmtNum(totalRx),
icon: <DownloadIcon className="size-4" />,
iconClassName: "text-info",
+1 -1
View File
@@ -3,7 +3,7 @@ import type { BgpSessionRow } from "@/lib/bgp/types"
import { fmtBgpNum } from "@/lib/bgp/helpers"
function BgpSessionDetail({ session }: { session: BgpSessionRow }) {
return <div className="px-4 pb-4 pt-2 bg-muted/20 border-t border-border/60">
return <div className="sticky left-0 w-[calc(100vw-2rem)] max-w-full md:static md:w-auto px-4 pb-4 pt-2 bg-muted/20 border-t border-border/60">
{session.state === "Unknown" && <p role="status" className="mb-3 text-xs text-muted-foreground">Состояние сессии неизвестно. Значения относятся к последнему полученному снимку.</p>}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
{[
@@ -14,6 +14,7 @@ import {
DATA_GRID_CELL_PAD_FIRST,
} from "@/components/data-grids/shared/data-grid-layout"
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
import { Button } from "@/components/ui/button"
import { EmptyState } from "@/components/empty-state"
import { BgpSessionDetail } from "@/components/data-grids/bgp-session-detail"
import type { BgpSessionRow, BgpState, BgpType } from "@/lib/bgp/types"
@@ -120,11 +121,10 @@ function BgpSessionsDataGrid({ sessions, emptyTitle, selectedSessionId, onSelect
const expanded = row.getIsExpanded()
return (
<div className="flex items-center gap-2">
{expanded ? (
<ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRightIcon className="size-3.5 shrink-0 text-muted-foreground/40" />
)}
<Button variant="ghost" size="icon-xs" aria-label={(expanded ? "Закрыть" : "Открыть") + " сессию " + row.original.serverLabel + " · " + row.original.peerIp} aria-expanded={expanded}
onClick={event => { event.stopPropagation(); if (onSelectSession) onSelectSession(expanded ? null : row.id); else row.toggleExpanded() }}>
{expanded ? <ChevronDownIcon className="size-3.5" /> : <ChevronRightIcon className="size-3.5" />}
</Button>
<span className="font-mono whitespace-nowrap">{row.original.serverLabel}</span>
</div>
)
@@ -261,7 +261,7 @@ function BgpSessionsDataGrid({ sessions, emptyTitle, selectedSessionId, onSelect
},
},
],
[],
[onSelectSession],
)
const table = useTable({
+10
View File
@@ -141,3 +141,13 @@ HTTP-фикстуры; layout/клавиатура проверены. Productio
Этап 9 завершён 2026-09-30 в границах локальной приёмки (209 E2E, frontend/backend build). Внешние ограничения — журнал 9.64. Следующий этап — маршрутизация/диагностика (10); схема и референсы сохраняются, сетевые правила применения не меняются.
Этап 10 начат 2026-09-30. Карта API, референсы, схема и совместимость: [ux-ui-stage-10.md](ux-ui-stage-10.md). Точная точка продолжения — в журнале.
## BGP — локальная визуальная приёмка 2026-09-30
| Экран / референс | ПК | Телефон | Схема |
| --- | --- | --- | --- |
| BGP · List 9 / Data Grid Filtering 2 | [Снимок](ux-ui/screens/bgp-desktop.png) | [Снимок](ux-ui/screens/bgp-mobile.png) | Устройство → свежесть → сводка → фильтры/сессии |
| BGP · Data Grid детали | [Детали](ux-ui/screens/bgp-detail-desktop.png) | [Детали](ux-ui/screens/bgp-detail-mobile.png) | URL сессии → подтверждённые поля / неизвестные значения |
| BGP · Stats 12 / Frame | [Аналитика](ux-ui/screens/bgp-analytics-desktop.png) | [Аналитика](ux-ui/screens/bgp-analytics-mobile.png) | Точный срез → достоверные суммы → сравнение |
27 E2E/сборки прошли; реальные RouterOS/JWT/БД не проверены. Далее — OSPF, точное продолжение в журнале.
+8 -1
View File
@@ -26,11 +26,18 @@
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.7, BGP фильтры URL; далее layout |
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.8, локальная приёмка BGP; далее OSPF |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 10.8 — визуальная и профильная приёмка BGP (2026-09-30)
- Два viewport 390×844/1440×1000, клавиатура и отсутствие общего переполнения проверены. Раскрытие сессии имеет явную кнопку с доступным именем/aria-expanded, Enter обновляет session URL. Выбранная сессия, скрытая фильтрами, имеет отдельное сообщение.
- Просмотрены шесть PNG BGP/деталей/аналитики. Визуальная проверка выявила обрезанные KPI и широкие детали на телефоне: подписи сокращены, детали ограничены viewport; таблица и state-фильтры сохраняют горизонтальную прокрутку. KpiStatGrid/Frame/Data Grid переиспользованы без изменений registry.
- 23 профильных E2E и четыре source-сценария прошли последовательно (27), четыре unit URL и server catalog/parser чистые. Frontend production build — 34 страницы, backend build, TypeScript/lint прошли.
- BGP завершён в границах локальных HTTP-фикстур и этих viewport. RouterOS/JWT/БД не проверены; общий accessibility/performance — 12. Старый неоднозначный BGP API/alert-engine остаётся для 11Б. Далее — OSPF: fetchServerOspf скрывает отдельные отказы BFD/routes; нужен строгий каталог и кэш с неизвестными состояниями. Квота на начале приёмки: 76% окна использовано. Локальный коммит без push.
## Блок 10.7 — URL-фильтры BGP (2026-09-30)
- Поиск, тип, состояние и завершённый FilterQuery хранятся в URL. Draft не сериализуется как готовое условие; поиск использует replace, изменения фильтра — push. Back/reload восстанавливают срез и сохраняют устройство/сессию/сторонние параметры.
+4
View File
@@ -68,3 +68,7 @@ server/session/tab и возврат/reload сохраняют контекст;
## Блок 10.7 — фильтры BGP URL
Поиск/тип/состояние/FilterQuery сохраняются и валидируются; AFI варианты соответствуют снимку. 21 E2E, четыре unit, TypeScript/lint прошли. Далее — desktop/mobile и сборка BGP, затем OSPF.
## Блок 10.8 — BGP приёмка
27 E2E, unit/TypeScript/lint, frontend/backend build прошли. Шесть PNG просмотрены, мобильные подписи/детали исправлены. Далее — строгий каталог OSPF (BFD/routes сейчас скрывают свои ошибки). Внешние границы — журнал.
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+36
View File
@@ -0,0 +1,36 @@
import { test, expect } from "@playwright/test"
import { mkdirSync } from "node:fs"
const server = (id: number) => ({ id, name: "Роутер " + id, host: "192.0.2." + id, enabled: true, country: "RU", status: null })
const session = (serverId: number, name = "Сессия " + serverId) => ({ id: "*1", serverId, serverName: "Роутер " + serverId, serverSite: "", serverCountry: "RU", name, peerIp: "192.0.2." + serverId, remoteAs: 64513, localAs: 64512, localId: "192.0.2.100", remoteId: "192.0.2." + serverId, state: "Established", type: "eBGP", uptime: "1h", holdTime: 90, keepalive: 30, prefixesRx: 12, prefixesTx: 2, inputMessages: 20, outputMessages: 10, capabilities: ["Тест capability"], lastError: null })
for (const device of ["desktop", "mobile"] as const) {
test("BGP: layout и клавиатура " + device, async ({ page }) => {
await page.setViewportSize(device === "desktop" ? { width: 1440, height: 1000 } : { width: 390, height: 844 })
await page.clock.install({ time: new Date("2026-09-30T05:00:00Z") })
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/servers" ? [server(1), server(2)] : path === "/api/bgp/catalog" ? { sessions: [session(1), session(2)], failures: [] } : path === "/api/auth/config" ? { required: false } : {} })
})
await page.goto("/bgp?server=2")
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
const scroll = page.getByRole("button", { name: /^Idle/ }).locator("..").first()
expect(await scroll.evaluate(el => el.getBoundingClientRect().right <= innerWidth)).toBe(true)
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" })
mkdirSync("docs/ux-ui/screens", { recursive: true })
await page.screenshot({ path: "docs/ux-ui/screens/bgp-" + device + ".png", animations: "disabled" })
const expand = page.getByRole("button", { name: "Открыть сессию Роутер 2 · 192.0.2.2", exact: true })
await expand.focus()
await page.keyboard.press("Enter")
await expect(page.getByText("Тест capability", { exact: true })).toBeVisible()
if (device === "mobile") expect(await page.getByText("Тест capability", { exact: true }).evaluate(el => el.getBoundingClientRect().right <= innerWidth)).toBe(true)
await expect.poll(() => new URL(page.url()).searchParams.get("session")).toBe("2-*1")
await page.screenshot({ path: "docs/ux-ui/screens/bgp-detail-" + device + ".png", animations: "disabled" })
await page.getByRole("button", { name: "Аналитика", exact: true }).focus()
await page.keyboard.press("Enter")
await expect(page.getByLabel("Сводка префиксов BGP")).toBeVisible()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.screenshot({ path: "docs/ux-ui/screens/bgp-analytics-" + device + ".png", animations: "disabled" })
})
}