Files
MikrotikManager/app/(main)/dashboard/page.tsx
T

347 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import Link from "next/link"
import {
ActivityIcon,
BellIcon,
CableIcon,
FilterIcon,
GitMergeIcon,
HardDriveIcon,
HeartPulseIcon,
MapIcon,
ServerIcon,
ShieldIcon,
} from "lucide-react"
import { PageHeader } from "@/components/page-header"
import { OpsPanel } from "@/components/ops-panel"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
import { QuickActionGrid } from "@/components/reui-kit/quick-action-grid"
import { AttentionQueue } from "@/components/reui-kit/attention-queue"
import { TrafficRxTxChart } from "@/components/reui-kit/traffic-rx-tx-chart"
import { LatencyChart } from "@/components/dashboard/latency-chart"
import { DashboardEventsTimeline } from "@/components/dashboard/dashboard-events-timeline"
import { InternetPathPanel } from "@/components/dashboard/internet-path-panel"
import { DashboardActiveProbesDataGrid } from "@/components/data-grids/dashboard-active-probes-data-grid"
import { EmptyState } from "@/components/empty-state"
import { DashboardDataStatus } from "@/components/dashboard/dashboard-data-status"
import { Badge } from "@/components/reui/badge"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { fmtRate } from "@/lib/fmt-rate"
import { useDashboardLive, type AttentionRow } from "@/hooks/use-dashboard-live"
function fmtIntRu(n: number): string {
return n.toLocaleString("ru-RU")
}
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.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>
)
}
export default function DashboardPage() {
const dash = useDashboardLive()
const offline = dash.offlineCount
const health = dash.probeDown + dash.probeWarn
return (
<div className="flex h-full flex-col">
<PageHeader crumbs={[{ label: "Обзор" }, { label: "Дашборд" }]} />
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
{dash.isLive && <DashboardDataStatus sources={dash.sources} refreshing={dash.refreshing} onRefresh={dash.retry} />}
<KpiStatGrid
aria-label="Сводка дашборда"
isLoading={dash.loading}
items={[
{
id: "fleet",
label: "Серверы онлайн",
value: dash.availability.servers ? `${dash.onlineCount} / ${dash.totalServers}` : "—",
hint: !dash.availability.servers ? "нет данных" : !dash.isLive
? "демо"
: offline > 0 || dash.unknownServerCount > 0 || dash.degradedServerCount > 0
? [offline > 0 ? `${offline} не в сети` : null,dash.unknownServerCount > 0 ? `Статус неизвестен: ${dash.unknownServerCount}` : null,dash.degradedServerCount > 0 ? `Ухудшение: ${dash.degradedServerCount}` : null].filter(Boolean).join(" · ")
: dash.totalServers > 0
? "все в сети"
: "нет серверов",
href: "/servers",
icon: <ServerIcon />,
iconClassName: "text-muted-foreground",
variant: offline > 0 || dash.unknownServerCount > 0 || dash.degradedServerCount > 0 ? "warning" : "default",
},
{
id: "overlay",
label: "Туннели работают",
value: dash.availability.overlay ? String(dash.overlay.up) : "—",
hint: !dash.availability.overlay ? "не все источники доступны" : dash.overlay.total > 0 ? `Не работают: ${dash.overlay.down}` : "нет туннелей",
href: "/gre",
icon: <CableIcon />,
iconClassName: "text-info",
variant: dash.overlay.down > 0 ? "warning" : "default",
},
{
id: "bgp",
label: "Префиксы BGP",
value: dash.bgp?.prefixSum != null ? fmtIntRu(dash.bgp.prefixSum) : "—",
hint: dash.bgp ? `Сессий установлено: ${dash.bgp.establishedCount ?? "неизвестно"}${dash.bgp.prefixSum === null ? " · префиксы неизвестны" : ""}` : "нет данных",
href: "/bgp",
icon: <GitMergeIcon />,
iconClassName: "text-primary",
},
{
id: "health",
label: "Проверки с проблемами",
value: dash.availability.probes && dash.unknownProbeCount===0 ? String(health) : "—",
hint: dash.availability.probes ? `${dash.probeDown} ошибок · ${dash.probeWarn} предупреждений${dash.unknownProbeCount>0?` · неизвестны: ${dash.unknownProbeCount}`:""}` : "нет данных",
href: "/uptime",
icon: <BellIcon />,
iconClassName: health > 0 ? "text-destructive" : "text-muted-foreground",
variant: health > 0 ? "destructive" : "default",
},
]}
/>
<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={<Link href="/traffic" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>Трафик →</Link>}
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>
) : (
<>
<div className="flex flex-wrap items-center gap-3 px-2 py-3 text-xs">
{dash.traffic.demo ? <Badge variant="outline" size="sm">демо</Badge> : !dash.traffic.observationsKnown ? <Badge variant="outline" size="sm">Измерения не подтверждены API</Badge> : null}
<span className="tabular-nums">RX {dash.traffic.rxNow===null?"—":fmtRate(dash.traffic.rxNow)}</span>
<span className="tabular-nums">TX {dash.traffic.txNow===null?"—":fmtRate(dash.traffic.txNow)}</span>
</div>
<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
title="Быстрые переходы"
description="Другие разделы управления сетью"
actions={[
{
id: "traffic",
title: "Трафик",
description: "Приём, передача и потоки IPFIX",
href: "/traffic",
icon: <ActivityIcon />,
iconClassName: "text-info",
},
{
id: "map",
title: "Карта сети",
description: "Топология GRE и WAN",
href: "/network-map",
icon: <MapIcon />,
iconClassName: "text-primary",
},
{
id: "filters",
title: "Фильтры",
description: "Правила обхода и списки",
href: "/filters",
icon: <FilterIcon />,
iconClassName: "text-info",
},
{
id: "firewall",
title: "Firewall",
description: "Цепочки filter / nat",
href: "/firewall",
icon: <ShieldIcon />,
iconClassName: "text-warning",
},
{
id: "backups",
title: "Бэкапы",
description: "Снапшоты и расписание",
href: "/backups",
icon: <HardDriveIcon />,
iconClassName: "text-muted-foreground",
},
{
id: "bgp",
title: "BGP",
description: "Сессии и префиксы",
href: "/bgp",
icon: <GitMergeIcon />,
iconClassName: "text-primary",
},
]}
/>
</div>
</div>
</div>
)
}