Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m13s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 14s
Co-authored-by: Cursor <[email protected]>
344 lines
13 KiB
TypeScript
344 lines
13 KiB
TypeScript
"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 { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert"
|
|
import { Badge } from "@/components/reui/badge"
|
|
import { Button, 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[] }) {
|
|
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>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const dash = useDashboardLive()
|
|
const offline = Math.max(0, dash.totalServers - dash.onlineCount)
|
|
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.error && !dash.loading ? (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>Не удалось обновить дашборд</AlertTitle>
|
|
<AlertDescription className="flex flex-wrap items-center gap-2">
|
|
<span>{dash.error}</span>
|
|
<Button variant="outline" size="sm" onClick={dash.retry}>
|
|
Повторить
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
<KpiStatGrid
|
|
aria-label="Сводка дашборда"
|
|
isLoading={dash.loading}
|
|
items={[
|
|
{
|
|
id: "fleet",
|
|
label: "Флот",
|
|
value: `${dash.onlineCount} / ${dash.totalServers}`,
|
|
hint: !dash.isLive
|
|
? "демо"
|
|
: offline > 0
|
|
? `${offline} offline`
|
|
: dash.totalServers > 0
|
|
? "все online"
|
|
: "нет узлов",
|
|
href: "/servers",
|
|
icon: <ServerIcon />,
|
|
iconClassName: "text-muted-foreground",
|
|
variant: offline > 0 ? "warning" : "default",
|
|
},
|
|
{
|
|
id: "overlay",
|
|
label: "Оверлей",
|
|
value: String(dash.overlay.up),
|
|
hint: dash.overlay.total > 0 ? `${dash.overlay.down} down` : "нет туннелей",
|
|
href: "/gre",
|
|
icon: <CableIcon />,
|
|
iconClassName: "text-info",
|
|
variant: dash.overlay.down > 0 ? "warning" : "default",
|
|
},
|
|
{
|
|
id: "bgp",
|
|
label: "BGP Rx",
|
|
value: dash.bgp ? fmtIntRu(dash.bgp.prefixSum) : "—",
|
|
hint: dash.bgp ? `${dash.bgp.establishedCount} Established` : "нет данных",
|
|
href: "/bgp",
|
|
icon: <GitMergeIcon />,
|
|
iconClassName: "text-primary",
|
|
},
|
|
{
|
|
id: "health",
|
|
label: "Здоровье",
|
|
value: String(health),
|
|
hint: `${dash.probeDown} down · ${dash.probeWarn} warn`,
|
|
href: "/uptime",
|
|
icon: <BellIcon />,
|
|
iconClassName: health > 0 ? "text-destructive" : "text-muted-foreground",
|
|
variant: health > 0 ? "destructive" : "default",
|
|
},
|
|
]}
|
|
/>
|
|
|
|
<QuickActionGrid
|
|
description="Разделы операционного контура"
|
|
actions={[
|
|
{
|
|
id: "traffic",
|
|
title: "Трафик",
|
|
description: "RX/TX флота и 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 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: "Все online",
|
|
emptyDescription: "Нет офлайн или degraded узлов.",
|
|
children: <AttentionRows rows={dash.attentionServers} />,
|
|
},
|
|
{
|
|
id: "overlay",
|
|
title: "Оверлей",
|
|
icon: CableIcon,
|
|
iconClassName: "text-info",
|
|
count: dash.attentionOverlay.length,
|
|
countVariant: "destructive-light",
|
|
emptyTitle: "Туннели up",
|
|
emptyDescription: "GRE и WireGuard в норме.",
|
|
children: <AttentionRows rows={dash.attentionOverlay} />,
|
|
},
|
|
{
|
|
id: "probes",
|
|
title: "Пробы",
|
|
icon: HeartPulseIcon,
|
|
iconClassName: "text-destructive",
|
|
count: dash.attentionProbes.length,
|
|
countVariant: "destructive-light",
|
|
emptyTitle: "Пробы up",
|
|
emptyDescription: "Нет 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="Нет проб на дашборде"
|
|
description="Включите пробу и отметьте ★ на странице мониторинга."
|
|
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>
|
|
)
|
|
}
|