fix(bgp): отображать только подтверждённые показатели сессий
This commit is contained in:
1 parent
7046808429
commit
8fdc4b917b
13 files changed
+148
-169
No files matched your search
@@ -1,130 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ClipboardCopyIcon } from "lucide-react"
|
||||
import type { BgpSessionRow } from "@/lib/bgp/types"
|
||||
import { fmtBgpNum, rscBgpSnippet } from "@/lib/bgp/helpers"
|
||||
|
||||
function CapChip({ cap }: { cap: string }) {
|
||||
return (
|
||||
<span className="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium border bg-muted/60 text-muted-foreground border-border/60">
|
||||
{cap}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function PrefixBar({ rx, tx, active }: { rx: number; tx: number; active: number }) {
|
||||
const max = Math.max(rx, 1)
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 text-[10px] font-mono">
|
||||
{[
|
||||
{ label: "Получено", val: rx, color: "bg-[var(--chart-rx)]", w: rx / max },
|
||||
{ label: "Активных", val: active, color: "bg-[var(--chart-1)]", w: active / max },
|
||||
{ label: "Отправлено", val: tx, color: "bg-[var(--chart-tx)]", w: Math.min(tx / max, 1) },
|
||||
].map((r) => (
|
||||
<div key={r.label} className="flex items-center gap-2">
|
||||
<span className="w-20 text-muted-foreground shrink-0">{r.label}</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className={cn("h-full rounded-full", r.color)}
|
||||
style={{ width: `${Math.max(r.w * 100, r.val > 0 ? 2 : 0)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-14 text-right tabular-nums">{fmtBgpNum(r.val)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { fmtBgpNum } from "@/lib/bgp/helpers"
|
||||
|
||||
function BgpSessionDetail({ session }: { session: BgpSessionRow }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
function copy() {
|
||||
navigator.clipboard.writeText(rscBgpSnippet(session)).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1800)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-4 pt-2 bg-muted/20 border-t border-border/60">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{[
|
||||
{ label: "Router ID", value: session.routerId },
|
||||
{ label: "Hold / KA", value: `${session.holdTime}s / ${session.keepalive}s` },
|
||||
{ label: "AFI/SAFI", value: session.afi },
|
||||
{
|
||||
label: "Сообщения ↓/↑",
|
||||
value: `${fmtBgpNum(session.inputMessages)} / ${fmtBgpNum(session.outputMessages)}`,
|
||||
},
|
||||
].map(({ label, value }) => (
|
||||
<div key={label}>
|
||||
<p className="text-[10px] text-muted-foreground mb-0.5">{label}</p>
|
||||
<p className="text-xs font-mono font-medium">{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{session.state === "Established" && (
|
||||
<div className="mb-4">
|
||||
<p className="text-[10px] text-muted-foreground mb-2 uppercase tracking-wider font-semibold">
|
||||
Префиксы
|
||||
</p>
|
||||
<PrefixBar
|
||||
rx={session.prefixesRx}
|
||||
tx={session.prefixesTx}
|
||||
active={session.prefixesActive}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{session.capabilities.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 uppercase tracking-wider font-semibold">
|
||||
Capabilities
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{session.capabilities.map((c) => (
|
||||
<CapChip key={c} cap={c} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{session.lastError && (
|
||||
<div className="mb-4 flex items-center gap-2 rounded-md border border-red-500/20 bg-red-500/5 px-3 py-2">
|
||||
<span className="size-1.5 rounded-full bg-red-500 shrink-0" />
|
||||
<p className="text-xs font-mono text-red-500">{session.lastError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-[10px] text-muted-foreground mb-1.5 uppercase tracking-wider font-semibold">
|
||||
RouterOS Export
|
||||
</p>
|
||||
<div className="rounded-md bg-[#0a0f1a] border border-white/8 px-3 py-2.5 flex items-start justify-between gap-3">
|
||||
<pre className="text-[10px] font-mono text-[#94a3b8] leading-relaxed whitespace-pre-wrap flex-1 min-w-0">
|
||||
{rscBgpSnippet(session)}
|
||||
</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className={cn(
|
||||
"shrink-0 flex items-center gap-1 text-[10px] px-2 py-1 rounded border transition-colors",
|
||||
copied
|
||||
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-400"
|
||||
: "border-white/10 text-white/40 hover:text-white/70 hover:border-white/20",
|
||||
)}
|
||||
>
|
||||
<ClipboardCopyIcon className="size-3" />
|
||||
{copied ? "Скопировано" : "Копировать"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
return <div className="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">
|
||||
{[
|
||||
{ label: "Router ID", value: session.routerId || "—" },
|
||||
{ label: "Hold / KA", value: (session.holdTime === null ? "—" : session.holdTime + "s") + " / " + (session.keepalive === null ? "—" : session.keepalive + "s") },
|
||||
{ label: "AFI/SAFI", value: session.afi },
|
||||
{ label: "Сообщения ↓/↑", value: fmtBgpNum(session.inputMessages) + " / " + fmtBgpNum(session.outputMessages) },
|
||||
{ label: "Префиксов получено", value: fmtBgpNum(session.prefixesRx) },
|
||||
{ label: "Префиксов отправлено", value: fmtBgpNum(session.prefixesTx) },
|
||||
{ label: "Активных маршрутов", value: session.prefixesActive === null ? "Нет данных API" : fmtBgpNum(session.prefixesActive) },
|
||||
].map(({ label, value }) => <div key={label}><p className="text-[10px] text-muted-foreground mb-0.5">{label}</p><p className="text-xs font-mono font-medium break-words">{value}</p></div>)}
|
||||
</div>
|
||||
)
|
||||
{session.capabilities.length > 0 && <div className="mb-4"><p className="text-[10px] text-muted-foreground mb-1.5">Capabilities</p><div className="flex flex-wrap gap-1.5">{session.capabilities.map((cap, i) => <span key={i} className="rounded px-1.5 py-0.5 text-[10px] border bg-muted/60 text-muted-foreground">{cap}</span>)}</div></div>}
|
||||
{session.lastError && <p className="mb-4 rounded-md border border-destructive/20 px-3 py-2 text-xs font-mono text-destructive break-words">{session.lastError}</p>}
|
||||
<p className="text-xs text-muted-foreground">API возвращает состояние сессии. Конфигурация connection, routing-table и фильтров для экспорта здесь не получена.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
export { BgpSessionDetail }
|
||||
@@ -223,7 +223,7 @@ function BgpSessionsDataGrid({ sessions, emptyTitle }: BgpSessionsDataGridProps)
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Prefixes ↓" />,
|
||||
cell: ({ row }) => {
|
||||
const n = row.original.prefixesRx
|
||||
return n > 0 ? (
|
||||
return n !== null ? (
|
||||
<span className="font-mono tabular-nums text-emerald-600 dark:text-emerald-400 text-right block">
|
||||
{fmtBgpNum(n)}
|
||||
</span>
|
||||
@@ -243,7 +243,7 @@ function BgpSessionsDataGrid({ sessions, emptyTitle }: BgpSessionsDataGridProps)
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Prefixes ↑" />,
|
||||
cell: ({ row }) => {
|
||||
const n = row.original.prefixesTx
|
||||
return n > 0 ? (
|
||||
return n !== null ? (
|
||||
<span className="font-mono tabular-nums text-[var(--chart-tx)] text-right block">
|
||||
{fmtBgpNum(n)}
|
||||
</span>
|
||||
|
||||
Reference in new issue
Block a user