Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
"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>
|
|
)
|
|
}
|
|
|
|
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>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { BgpSessionDetail }
|