fix(ui): поднять очередь проблем и уточнить сводку дашборда
This commit is contained in:
1 parent
fe9df167ec
commit
4ec32765df
5 files changed
+245
-190
No files matched your search
+194
-177
@@ -36,24 +36,34 @@ function fmtIntRu(n: number): string {
|
||||
}
|
||||
|
||||
function AttentionRows({ rows }: { rows: AttentionRow[] }) {
|
||||
function renderRow(row: AttentionRow) {
|
||||
return (
|
||||
<Link
|
||||
key={row.id}
|
||||
href={row.href}
|
||||
className="hover:bg-muted/40 focus-visible:ring-ring flex min-w-0 items-start justify-between gap-2 rounded-md px-1 py-1.5 focus-visible:ring-2"
|
||||
>
|
||||
<span className="min-w-0 wrap-anywhere text-sm font-medium">{row.title}</span>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant={row.tone === "warning" ? "warning-light" : "destructive-light"}
|
||||
className="shrink-0"
|
||||
>
|
||||
{row.hint}
|
||||
</Badge>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{rows.map((row) => (
|
||||
<Link
|
||||
key={row.id}
|
||||
href={row.href}
|
||||
className="hover:bg-muted/40 flex min-w-0 items-center justify-between gap-2 rounded-md px-1 py-1.5"
|
||||
>
|
||||
<span className="truncate text-sm font-medium">{row.title}</span>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant={row.tone === "warning" ? "warning-light" : "destructive-light"}
|
||||
className="shrink-0"
|
||||
>
|
||||
{row.hint}
|
||||
</Badge>
|
||||
</Link>
|
||||
))}
|
||||
{rows.slice(0, 5).map(renderRow)}
|
||||
{rows.length > 5 && (
|
||||
<details>
|
||||
<summary className="cursor-pointer rounded-md px-1 py-2 text-xs text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring">Ещё {rows.length - 5}</summary>
|
||||
{rows.slice(5).map(renderRow)}
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -77,15 +87,15 @@ export default function DashboardPage() {
|
||||
items={[
|
||||
{
|
||||
id: "fleet",
|
||||
label: "Флот",
|
||||
label: "Серверы онлайн",
|
||||
value: dash.availability.servers ? `${dash.onlineCount} / ${dash.totalServers}` : "—",
|
||||
hint: !dash.availability.servers ? "нет данных" : !dash.isLive
|
||||
? "демо"
|
||||
: offline > 0
|
||||
? `${offline} offline`
|
||||
? `${offline} не в сети`
|
||||
: dash.totalServers > 0
|
||||
? "все online"
|
||||
: "нет узлов",
|
||||
? "все в сети"
|
||||
: "нет серверов",
|
||||
href: "/servers",
|
||||
icon: <ServerIcon />,
|
||||
iconClassName: "text-muted-foreground",
|
||||
@@ -93,9 +103,9 @@ export default function DashboardPage() {
|
||||
},
|
||||
{
|
||||
id: "overlay",
|
||||
label: "Оверлей",
|
||||
label: "Туннели работают",
|
||||
value: dash.availability.overlay ? String(dash.overlay.up) : "—",
|
||||
hint: !dash.availability.overlay ? "не все источники доступны" : dash.overlay.total > 0 ? `${dash.overlay.down} down` : "нет туннелей",
|
||||
hint: !dash.availability.overlay ? "не все источники доступны" : dash.overlay.total > 0 ? `Не работают: ${dash.overlay.down}` : "нет туннелей",
|
||||
href: "/gre",
|
||||
icon: <CableIcon />,
|
||||
iconClassName: "text-info",
|
||||
@@ -103,18 +113,18 @@ export default function DashboardPage() {
|
||||
},
|
||||
{
|
||||
id: "bgp",
|
||||
label: "BGP Rx",
|
||||
label: "Префиксы BGP",
|
||||
value: dash.bgp ? fmtIntRu(dash.bgp.prefixSum) : "—",
|
||||
hint: dash.bgp ? `${dash.bgp.establishedCount} Established` : "нет данных",
|
||||
hint: dash.bgp ? `Сессий установлено: ${dash.bgp.establishedCount}` : "нет данных",
|
||||
href: "/bgp",
|
||||
icon: <GitMergeIcon />,
|
||||
iconClassName: "text-primary",
|
||||
},
|
||||
{
|
||||
id: "health",
|
||||
label: "Здоровье",
|
||||
label: "Проверки с проблемами",
|
||||
value: dash.availability.probes ? String(health) : "—",
|
||||
hint: dash.availability.probes ? `${dash.probeDown} down · ${dash.probeWarn} warn` : "нет данных",
|
||||
hint: dash.availability.probes ? `${dash.probeDown} ошибок · ${dash.probeWarn} предупреждений` : "нет данных",
|
||||
href: "/uptime",
|
||||
icon: <BellIcon />,
|
||||
iconClassName: health > 0 ? "text-destructive" : "text-muted-foreground",
|
||||
@@ -123,13 +133,169 @@ export default function DashboardPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<section aria-labelledby="dashboard-attention-title" className="flex min-w-0 flex-col gap-2">
|
||||
<h2 id="dashboard-attention-title" className="text-sm font-semibold">Требует внимания</h2>
|
||||
<AttentionQueue
|
||||
className="lg:grid-cols-3"
|
||||
columns={[
|
||||
{
|
||||
id: "nodes",
|
||||
title: "Серверы",
|
||||
icon: ServerIcon,
|
||||
count: dash.attentionServers.length,
|
||||
countVariant: "warning-light",
|
||||
emptyTitle: dash.reliable.servers ? (dash.totalServers > 0 ? "Серверы без проблем" : "Нет серверов") : "Состояние не подтверждено",
|
||||
emptyDescription: dash.reliable.servers ? "Нет включённых серверов с ошибками доступности." : "Проверьте состояние источников выше.",
|
||||
children: <AttentionRows rows={dash.attentionServers} />,
|
||||
},
|
||||
{
|
||||
id: "overlay",
|
||||
title: "Туннели",
|
||||
icon: CableIcon,
|
||||
iconClassName: "text-info",
|
||||
count: dash.attentionOverlay.length,
|
||||
countVariant: "destructive-light",
|
||||
emptyTitle: dash.reliable.overlay ? (dash.overlay.total > 0 ? "Туннели работают" : "Нет туннелей") : "Состояние не подтверждено",
|
||||
emptyDescription: dash.reliable.overlay ? "В GRE, WireGuard и VXLAN проблем не обнаружено." : "Проверьте состояние источников выше.",
|
||||
children: <AttentionRows rows={dash.attentionOverlay} />,
|
||||
},
|
||||
{
|
||||
id: "probes",
|
||||
title: "Проверки",
|
||||
icon: HeartPulseIcon,
|
||||
iconClassName: "text-destructive",
|
||||
count: dash.attentionProbes.length,
|
||||
countVariant: "destructive-light",
|
||||
emptyTitle: dash.reliable.probes ? (dash.probeCount > 0 ? "Проверки без ошибок" : "Нет активных проверок") : "Состояние не подтверждено",
|
||||
emptyDescription: dash.reliable.probes ? "Нет ошибок и предупреждений активных проверок." : "Проверьте состояние источников выше.",
|
||||
children: <AttentionRows rows={dash.attentionProbes} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_1fr]">
|
||||
<OpsPanel
|
||||
title="Задержка до серверов"
|
||||
description={
|
||||
dash.latency.kind === "mock" || dash.latency.kind === "live"
|
||||
? dash.latency.subtitle
|
||||
: dash.latency.kind === "loading"
|
||||
? "Загрузка…"
|
||||
: "Нет серии RTT"
|
||||
}
|
||||
contentClassName="px-3 pb-3"
|
||||
>
|
||||
{dash.latency.kind === "loading" && (
|
||||
<div className="bg-muted/50 h-[220px] w-full animate-pulse rounded-md" />
|
||||
)}
|
||||
{dash.latency.kind === "empty" && (
|
||||
<p className="text-muted-foreground px-4 py-10 text-center text-sm">{dash.latency.message}</p>
|
||||
)}
|
||||
{(dash.latency.kind === "mock" || dash.latency.kind === "live") && (
|
||||
<LatencyChart series={dash.latency.series} labels={dash.latency.labels} />
|
||||
)}
|
||||
</OpsPanel>
|
||||
|
||||
<OpsPanel
|
||||
title="Последние события"
|
||||
description="Система и BGP"
|
||||
headerRight={
|
||||
<Link href="/alerts" className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "h-7 text-xs")}>
|
||||
Все →
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<DashboardEventsTimeline
|
||||
events={dash.recentEvents}
|
||||
loading={dash.eventsLoading}
|
||||
error={dash.eventsError}
|
||||
/>
|
||||
</OpsPanel>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<OpsPanel
|
||||
title="Пропускная способность"
|
||||
description={dash.traffic?.demo ? "Суммарный RX / TX · демо" : "Суммарный RX / TX серверов · 1 ч"}
|
||||
headerRight={
|
||||
dash.traffic ? (
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{dash.traffic.demo ? <Badge variant="outline" size="sm">демо</Badge> : null}
|
||||
<span className="tabular-nums">RX {fmtRate(dash.traffic.rxNow)}</span>
|
||||
<span className="tabular-nums">TX {fmtRate(dash.traffic.txNow)}</span>
|
||||
<Link href="/traffic" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>
|
||||
Трафик →
|
||||
</Link>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
contentClassName="px-3 pb-3"
|
||||
>
|
||||
{dash.loading && !dash.traffic ? (
|
||||
<div className="bg-muted/50 h-[220px] w-full animate-pulse rounded-md" />
|
||||
) : !dash.traffic ? (
|
||||
<p className="text-muted-foreground px-4 py-10 text-center text-sm">
|
||||
Нет данных трафика за последний час.{" "}
|
||||
<Link href="/traffic" className="underline underline-offset-2">
|
||||
Открыть трафик
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
<TrafficRxTxChart rx={dash.traffic.rx} tx={dash.traffic.tx} range="1h" embedded />
|
||||
)}
|
||||
</OpsPanel>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<InternetPathPanel
|
||||
model={dash.internetPath}
|
||||
loading={dash.internetPathLoading}
|
||||
error={dash.isLive ? dash.internetPathError : null}
|
||||
/>
|
||||
|
||||
<OpsPanel
|
||||
title="Активные пробы"
|
||||
description={
|
||||
<span className={dash.error && dash.isLive ? "text-destructive" : undefined}>{dash.probesSubtitle}</span>
|
||||
}
|
||||
headerRight={
|
||||
<Link href="/uptime" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>
|
||||
Мониторинг →
|
||||
</Link>
|
||||
}
|
||||
contentClassName="px-0 pb-0"
|
||||
>
|
||||
{dash.activeProbes.length === 0 && !dash.probesLoading ? (
|
||||
<EmptyState
|
||||
className="py-10"
|
||||
title={dash.availability.probes ? "Нет проб на дашборде" : "Данные проб недоступны"}
|
||||
description={dash.availability.probes ? "Включите пробу и отметьте ★ на странице мониторинга." : "Повторите обновление данных."}
|
||||
action={
|
||||
<Link href="/uptime" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
|
||||
Открыть мониторинг
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DashboardActiveProbesDataGrid
|
||||
probes={dash.activeProbes}
|
||||
catalog={dash.servers}
|
||||
isLoading={dash.probesLoading}
|
||||
emptyDescription="Нет проб с звездой на дашборде."
|
||||
/>
|
||||
)}
|
||||
</OpsPanel>
|
||||
|
||||
<QuickActionGrid
|
||||
description="Разделы операционного контура"
|
||||
title="Быстрые переходы"
|
||||
description="Другие разделы управления сетью"
|
||||
actions={[
|
||||
{
|
||||
id: "traffic",
|
||||
title: "Трафик",
|
||||
description: "RX/TX флота и IPFIX",
|
||||
description: "Приём, передача и потоки IPFIX",
|
||||
href: "/traffic",
|
||||
icon: <ActivityIcon />,
|
||||
iconClassName: "text-info",
|
||||
@@ -177,155 +343,6 @@ export default function DashboardPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_1fr]">
|
||||
<OpsPanel
|
||||
title="Задержка до серверов"
|
||||
description={
|
||||
dash.latency.kind === "mock" || dash.latency.kind === "live"
|
||||
? dash.latency.subtitle
|
||||
: dash.latency.kind === "loading"
|
||||
? "Загрузка…"
|
||||
: "Нет серии RTT"
|
||||
}
|
||||
contentClassName="px-3 pb-3"
|
||||
>
|
||||
{dash.latency.kind === "loading" && (
|
||||
<div className="bg-muted/50 h-[220px] w-full animate-pulse rounded-md" />
|
||||
)}
|
||||
{dash.latency.kind === "empty" && (
|
||||
<p className="text-muted-foreground px-4 py-10 text-center text-sm">{dash.latency.message}</p>
|
||||
)}
|
||||
{(dash.latency.kind === "mock" || dash.latency.kind === "live") && (
|
||||
<LatencyChart series={dash.latency.series} labels={dash.latency.labels} />
|
||||
)}
|
||||
</OpsPanel>
|
||||
|
||||
<OpsPanel
|
||||
title="Последние события"
|
||||
description="Система и BGP"
|
||||
headerRight={
|
||||
<Link href="/alerts" className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "h-7 text-xs")}>
|
||||
Все →
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<DashboardEventsTimeline
|
||||
events={dash.recentEvents}
|
||||
loading={dash.eventsLoading}
|
||||
error={dash.eventsError}
|
||||
/>
|
||||
</OpsPanel>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[3fr_2fr]">
|
||||
<OpsPanel
|
||||
title="Пропускная способность"
|
||||
description={dash.traffic?.demo ? "Суммарный RX / TX · демо" : "Суммарный RX / TX флота · 1 ч"}
|
||||
headerRight={
|
||||
dash.traffic ? (
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{dash.traffic.demo ? <Badge variant="outline" size="sm">демо</Badge> : null}
|
||||
<span className="tabular-nums">RX {fmtRate(dash.traffic.rxNow)}</span>
|
||||
<span className="tabular-nums">TX {fmtRate(dash.traffic.txNow)}</span>
|
||||
<Link href="/traffic" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>
|
||||
Трафик →
|
||||
</Link>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
contentClassName="px-3 pb-3"
|
||||
>
|
||||
{dash.loading && !dash.traffic ? (
|
||||
<div className="bg-muted/50 h-[220px] w-full animate-pulse rounded-md" />
|
||||
) : !dash.traffic ? (
|
||||
<p className="text-muted-foreground px-4 py-10 text-center text-sm">
|
||||
Нет данных трафика за последний час.{" "}
|
||||
<Link href="/traffic" className="underline underline-offset-2">
|
||||
Открыть трафик
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
<TrafficRxTxChart rx={dash.traffic.rx} tx={dash.traffic.tx} range="1h" embedded />
|
||||
)}
|
||||
</OpsPanel>
|
||||
|
||||
<AttentionQueue
|
||||
className="@3xl:grid-cols-1"
|
||||
columns={[
|
||||
{
|
||||
id: "nodes",
|
||||
title: "Узлы",
|
||||
icon: ServerIcon,
|
||||
count: dash.attentionServers.length,
|
||||
countVariant: "warning-light",
|
||||
emptyTitle: dash.reliable.servers ? "Все online" : "Состояние не подтверждено",
|
||||
emptyDescription: dash.reliable.servers ? "Нет офлайн или degraded узлов." : "Проверьте состояние источников выше.",
|
||||
children: <AttentionRows rows={dash.attentionServers} />,
|
||||
},
|
||||
{
|
||||
id: "overlay",
|
||||
title: "Оверлей",
|
||||
icon: CableIcon,
|
||||
iconClassName: "text-info",
|
||||
count: dash.attentionOverlay.length,
|
||||
countVariant: "destructive-light",
|
||||
emptyTitle: dash.reliable.overlay ? "Туннели up" : "Состояние не подтверждено",
|
||||
emptyDescription: dash.reliable.overlay ? "GRE и WireGuard в норме." : "Проверьте состояние источников выше.",
|
||||
children: <AttentionRows rows={dash.attentionOverlay} />,
|
||||
},
|
||||
{
|
||||
id: "probes",
|
||||
title: "Пробы",
|
||||
icon: HeartPulseIcon,
|
||||
iconClassName: "text-destructive",
|
||||
count: dash.attentionProbes.length,
|
||||
countVariant: "destructive-light",
|
||||
emptyTitle: dash.reliable.probes ? "Пробы up" : "Состояние не подтверждено",
|
||||
emptyDescription: dash.reliable.probes ? "Нет down или warn." : "Проверьте состояние источников выше.",
|
||||
children: <AttentionRows rows={dash.attentionProbes} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InternetPathPanel
|
||||
model={dash.internetPath}
|
||||
loading={dash.internetPathLoading}
|
||||
error={dash.isLive ? dash.internetPathError : null}
|
||||
/>
|
||||
|
||||
<OpsPanel
|
||||
title="Активные пробы"
|
||||
description={
|
||||
<span className={dash.error && dash.isLive ? "text-destructive" : undefined}>{dash.probesSubtitle}</span>
|
||||
}
|
||||
headerRight={
|
||||
<Link href="/uptime" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>
|
||||
Мониторинг →
|
||||
</Link>
|
||||
}
|
||||
contentClassName="px-0 pb-0"
|
||||
>
|
||||
{dash.activeProbes.length === 0 && !dash.probesLoading ? (
|
||||
<EmptyState
|
||||
className="py-10"
|
||||
title={dash.availability.probes ? "Нет проб на дашборде" : "Данные проб недоступны"}
|
||||
description={dash.availability.probes ? "Включите пробу и отметьте ★ на странице мониторинга." : "Повторите обновление данных."}
|
||||
action={
|
||||
<Link href="/uptime" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
|
||||
Открыть мониторинг
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DashboardActiveProbesDataGrid
|
||||
probes={dash.activeProbes}
|
||||
catalog={dash.servers}
|
||||
isLoading={dash.probesLoading}
|
||||
emptyDescription="Нет проб с звездой на дашборде."
|
||||
/>
|
||||
)}
|
||||
</OpsPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+13
-1
@@ -20,7 +20,7 @@
|
||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
|
||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||
| 6 | Навигация и дашборд | В работе: адаптивная шапка готова, далее навигация и очередь проблем |
|
||||
| 6 | Навигация и дашборд | В работе: шапка, поиск и иерархия готовы; остаются переходы к объектам |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||
| 8 | Трафик и статистика | Не начат |
|
||||
| 9 | VPN, Firewall и Filters | Не начат |
|
||||
@@ -399,6 +399,18 @@ CommandPalette использует установленные Command/cmdk и B
|
||||
|
||||
Блок готов для локального коммита без push. Этап 6 продолжается: иерархия дашборда, затем переходы из очереди к конкретным объектам.
|
||||
|
||||
## Этап 6 — блок 3: сводка и приоритет проблем (2026-09-20)
|
||||
|
||||
Дашборд сохраняет KpiStatGrid, AttentionQueue и Frame, но очередь «Требует внимания» теперь идёт сразу после сводки, до измерений. Быстрые переходы перенесены вниз. Подписи заменены на «Серверы онлайн», «Туннели работают», «Префиксы BGP», «Проверки с проблемами»; состояния в очереди названы по-русски. Длинные имена проблем переносятся вместо обрезки. Состояния пустого каталога серверов, отсутствия туннелей и активных проверок отделены от отсутствия ошибок.
|
||||
|
||||
useDashboardLive больше не обрезает массивы проблем до пяти до подсчёта. Счётчики учитывают все элементы; UI сначала показывает пять и раскрывает остальные через «Ещё N» с клавиатуры. Загрузка/частичные сбои/свежесть/защита от поздних ответов этапа 5 сохранены. Запросы и сетевые операции не менялись.
|
||||
|
||||
Приёмка текущей общей основы: **65 Playwright-тестов passed**, включая 6 сценариев дашборда, 4 навигации и 4 шапки; отдельные профильные запуски также успешны. Новый сценарий проверяет 7 проблем при 5 видимых строках, раскрытие клавиатурой, порядок очереди перед измерениями, пустые активные проверки и ширину 390. Просмотрены скриншоты дашборда 1440/390 и поиска в двух темах. **Production build успешен: контракты, TypeScript, 34 страницы.** Scoped ESLint нового кода успешен; исходная ошибка AppSidebar описана в блоке 2. ReUI checklist получен; vendored-примитивы, зависимости и тема не менялись. Проверки используют HTTP-моки, реальный RouterOS не проверен.
|
||||
|
||||
**Этап 6 ещё не завершён.** Ссылки AttentionRow пока открывают профильный раздел (/servers, /uptime, /gre, /wireguard, /vxlan), без выбора конкретного объекта. Следующий точный шаг: спроектировать единый URL-контракт идентификатора объекта и сервера, подключить принимающие страницы и только после этого заменить href очереди. Начать с сервера и проверить «дашборд → конкретный сервер → назад», прямое открытие URL, удалённый/недоступный объект, затем адаптировать мониторинг и туннели. Не считать query-параметр рабочим, пока целевая страница его не обрабатывает. Полные формы/права/фильтры URL этапа 7 остаются следующим этапом.
|
||||
|
||||
Границы: текущий mapper дашборда по-прежнему трактует null server.status как offline; неизвестные состояния устройств требуют отдельного исправления и проверки. Квота при завершении общей регрессии и запуске build: использовано 65% пятичасовой и 26% недельной. Сохранить блок локальным коммитом без push; изменения skills/AGENTS/.codex не включать.
|
||||
|
||||
## Карта миграции (исходная инвентаризация этапа 0)
|
||||
|
||||
Исходная связка на этапе 0: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. По итогам этапа 4 используется @tanstack/react-table 9.2.4 с useTable/dataGridFeatures и дерево FilterQuery вместо старого filters/onChange. Список ниже сохранён как историческая инвентаризация.
|
||||
|
||||
@@ -42,3 +42,7 @@
|
||||
4. **Приёмка.** Просмотреть ПК/телефон, проверить переходы, пустое состояние, частичный сбой и клавиатуру; профильные тесты, TypeScript, lint и production build общей оболочки.
|
||||
|
||||
HTTP-моки проверяют интерфейс и запросы. Реальные данные, права и RouterOS требуют тестового окружения. Формы и полный URL-контекст сервера относятся к этапу 7; сетевые операции в этапе 6 не менять.
|
||||
|
||||
## Состояние реализации
|
||||
|
||||
Шапка и навигация завершены. Иерархия дашборда, подписи и полные счётчики проблем проверены. Остаются ссылки очереди на конкретные объекты и их приёмка на принимающих страницах. Общая регрессия текущего состояния: 65 сценариев passed; production build успешен. Подробнее — ux-ui-progress.md.
|
||||
@@ -630,11 +630,10 @@ export function useDashboardLive() {
|
||||
const attentionServers: AttentionRow[] = useMemo(() => {
|
||||
return servers
|
||||
.filter((s) => s.enabled && s.status !== "online")
|
||||
.slice(0, 5)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.name,
|
||||
hint: s.status === "degraded" ? "degraded" : "offline",
|
||||
hint: s.status === "degraded" ? "Ухудшение" : "Не в сети",
|
||||
href: "/servers",
|
||||
tone: s.status === "degraded" ? ("warning" as const) : ("destructive" as const),
|
||||
}))
|
||||
@@ -643,11 +642,10 @@ export function useDashboardLive() {
|
||||
const attentionOverlay: AttentionRow[] = useMemo(() => {
|
||||
return overlay.items
|
||||
.filter((i) => i.status !== "up")
|
||||
.slice(0, 5)
|
||||
.map((i) => ({
|
||||
id: i.id,
|
||||
title: i.name,
|
||||
hint: i.status === "degraded" ? "degraded" : "down",
|
||||
hint: i.status === "degraded" ? "Ухудшение" : "Не работает",
|
||||
href: i.href,
|
||||
tone: i.status === "degraded" ? ("warning" as const) : ("destructive" as const),
|
||||
}))
|
||||
@@ -656,11 +654,10 @@ export function useDashboardLive() {
|
||||
const attentionProbes: AttentionRow[] = useMemo(() => {
|
||||
return enabledProbes
|
||||
.filter((p) => p.status === "down" || p.status === "warn")
|
||||
.slice(0, 5)
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
title: p.name,
|
||||
hint: p.status === "warn" ? "warn" : "down",
|
||||
hint: p.status === "warn" ? "Предупреждение" : "Ошибка",
|
||||
href: "/uptime",
|
||||
tone: p.status === "warn" ? ("warning" as const) : ("destructive" as const),
|
||||
}))
|
||||
@@ -714,6 +711,7 @@ export function useDashboardLive() {
|
||||
totalServers: servers.length,
|
||||
probeDown,
|
||||
probeWarn,
|
||||
probeCount: enabledProbes.length,
|
||||
latency,
|
||||
recentEvents,
|
||||
eventsLoading,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { test, expect, type Page } from "@playwright/test"
|
||||
|
||||
async function setup(page: Page, initiallyFailed = false) {
|
||||
async function setup(page: Page, initiallyFailed = false, serverCount = 1) {
|
||||
const control = { failed: initiallyFailed, partial: false, prefixes: 321, hold: false, holdPath: "/api/bgp/sessions", fallback: false, failFallback: false, pending: 0, release: () => {} }
|
||||
let release!: () => void
|
||||
const gate = new Promise<void>(resolve => { release = resolve })
|
||||
control.release = release
|
||||
const servers = [{ id: 1, name: "Узел", host: "192.0.2.1", site: "TEST", country: "RU", asn: "64512", type: "exit-node", enabled: true, status: "online", latency: 10, os: "7", model: "CHR" }]
|
||||
const servers = Array.from({ length: serverCount }, (_, index) => ({ id: index + 1, name: serverCount === 1 ? "Узел" : `Узел ${index + 1}`, host: `192.0.2.${index + 1}`, site: "TEST", country: "RU", asn: "64512", type: "exit-node", enabled: true, status: serverCount === 1 ? "online" : "offline", latency: 10, os: "7", model: "CHR" }))
|
||||
const snapshot = { sampledAt: "2026-09-20T00:00:00Z", servers, greTunnels: [], filtersRulesets: [], speedProbes: [], routeLookupByServerId: {}, wanRuntimeByHomeId: {} }
|
||||
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", async route => {
|
||||
@@ -38,16 +38,40 @@ async function setup(page: Page, initiallyFailed = false) {
|
||||
}
|
||||
|
||||
const refresh = (page: Page) => page.getByRole("button", { name: "Обновить данные", exact: true })
|
||||
const bgp = (page: Page) => page.getByRole("link").filter({ hasText: "BGP Rx" })
|
||||
const bgp = (page: Page) => page.getByRole("link").filter({ hasText: "Префиксы BGP" })
|
||||
|
||||
test("Дашборд: очередь над измерениями, полный счётчик и раскрытие остальных проблем", async ({ page }) => {
|
||||
await setup(page, false, 7)
|
||||
await expect(refresh(page)).toBeEnabled()
|
||||
const queue = page.getByRole("region", { name: "Требует внимания", exact: true })
|
||||
await expect(queue.getByText("7", { exact: true })).toBeVisible()
|
||||
const serverLinks = queue.getByRole("link", { name: /^Узел \d/ })
|
||||
await expect(serverLinks).toHaveCount(5)
|
||||
const more = queue.locator("summary").filter({ hasText: "Ещё 2" })
|
||||
await more.focus()
|
||||
await more.press("Enter")
|
||||
await expect(serverLinks).toHaveCount(7)
|
||||
await more.press("Enter")
|
||||
await expect(serverLinks).toHaveCount(5)
|
||||
const queueBox = await queue.boundingBox()
|
||||
const measurementBox = await page.getByText("Задержка до серверов", { exact: true }).boundingBox()
|
||||
expect(queueBox!.y + queueBox!.height).toBeLessThan(measurementBox!.y)
|
||||
await expect(queue.getByText("Нет активных проверок", { exact: true })).toBeVisible()
|
||||
await page.screenshot({ path: "test-results/dashboard-priority-desktop.png", animations: "disabled" })
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.getByRole("navigation", { name: "Путь страницы" }).scrollIntoViewIfNeeded()
|
||||
await page.screenshot({ path: "test-results/dashboard-priority-mobile.png", animations: "disabled" })
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
||||
})
|
||||
|
||||
test("Дашборд: первоначальная ошибка не подменяется нулями и исправным состоянием", async ({ page }) => {
|
||||
const control = await setup(page, true)
|
||||
await expect(refresh(page)).toBeEnabled()
|
||||
await expect(page.getByText("Не обновлены источники: 9", { exact: true }).first()).toBeVisible()
|
||||
await expect(page.getByRole("link").filter({ hasText: "Флот" })).toContainText("—")
|
||||
await expect(page.getByRole("link").filter({ hasText: "Серверы онлайн" })).toContainText("—")
|
||||
await expect(bgp(page)).toContainText("—")
|
||||
await expect(page.getByText("Все online", { exact: true })).toHaveCount(0)
|
||||
await expect(page.getByText("Пробы up", { exact: true })).toHaveCount(0)
|
||||
await expect(page.getByText("Серверы без проблем", { exact: true })).toHaveCount(0)
|
||||
await expect(page.getByText("Проверки без ошибок", { exact: true })).toHaveCount(0)
|
||||
control.failed = false
|
||||
await refresh(page).click()
|
||||
await expect(bgp(page)).toContainText("321")
|
||||
|
||||
Reference in new issue
Block a user