fix(bgp): отображать только подтверждённые показатели сессий

This commit is contained in:
Denozordec committed 2026-09-30 16:56:28 +07:00
1 parent 7046808429
commit 8fdc4b917b
13 files changed
+148 -169

No files matched your search

+24 -26
View File
@@ -11,6 +11,7 @@ import { applyFilterQuery } from "@/lib/data-filters/apply-filter-query"
import { BGP_FILTER_FIELDS } from "@/lib/data-filters/bgp-filter-fields"
import type { BgpSessionRow, BgpState, BgpType } from "@/lib/bgp/types"
import { bgpCatalogSchema, bgpServersSchema, type BackendBgpSession, type BgpFailure } from "@/lib/bgp/response"
import { fmtBgpNum, sumBgpCounters } from "@/lib/bgp/helpers"
import { BGP_AS_NAMES } from "@/lib/bgp/types"
import { Frame, FramePanel } from "@/components/reui/frame"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
@@ -210,11 +211,7 @@ function TypeBadge({ type }: { type: BgpType }) {
}
function fmtNum(n: number) {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`
return String(n)
}
const fmtNum = fmtBgpNum
// ─── backend mapping ──────────────────────────────────────────────────────────
@@ -239,13 +236,13 @@ function backendToFrontend(b: BackendBgpSession): BgpSession {
description: b.name,
state: Object.hasOwn(STATE_STYLE, b.state) ? b.state as BgpState : "Unknown",
type: b.type,
afi: "IPv4 Unicast",
afi: b.afi ?? "Неизвестно",
uptime: b.uptime,
holdTime: b.holdTime,
keepalive: b.keepalive,
prefixesRx: b.prefixesRx,
prefixesTx: b.prefixesTx,
prefixesActive: b.prefixesRx,
prefixesActive: null,
inputMessages: b.inputMessages,
outputMessages: b.outputMessages,
capabilities: b.capabilities,
@@ -349,8 +346,8 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) {
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{byRouter.map(router => {
const estCnt = router.sessions.filter(s => s.state === "Established").length
const downCnt = router.sessions.length - estCnt
const totalRx = router.sessions.reduce((a, s) => a + s.prefixesRx, 0)
const downCnt = router.sessions.filter(s => s.state !== "Established" && s.state !== "Unknown").length
const totalRx = sumBgpCounters(router.sessions.map(s => s.prefixesRx))
return (
<Frame key={router.id} dense className="w-full overflow-hidden">
@@ -360,7 +357,7 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) {
<ServerIcon className="size-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium leading-none">{router.label}</p>
<p className="text-[11px] font-mono text-muted-foreground mt-0.5">{router.site} · AS65001</p>
<p className="text-[11px] font-mono text-muted-foreground mt-0.5">{router.site} · {Array.from(new Set(router.sessions.map(s => "AS" + s.localAs))).join(", ")}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
{estCnt > 0 && (
@@ -398,12 +395,12 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) {
</div>
</div>
<div className="shrink-0 text-right">
{s.prefixesRx > 0 && (
{s.prefixesRx !== null && s.prefixesRx > 0 && (
<p className="text-[10px] font-mono text-emerald-500 tabular-nums">
↓ {fmtNum(s.prefixesRx)}
</p>
)}
<p className={cn("text-[10px] font-semibold", ss.text)}>{s.state}</p>
<p className={cn("text-[10px] font-semibold", ss.text)}>{ss.label}</p>
</div>
</div>
)
@@ -411,7 +408,7 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) {
</div>
{/* footer summary */}
{totalRx > 0 && (
{totalRx !== null && totalRx > 0 && (
<div className="flex items-center gap-2 px-4 py-2 border-t bg-muted/20">
<ArrowDownIcon className="size-3 text-emerald-500" />
<span className="text-[11px] font-mono text-muted-foreground">
@@ -432,9 +429,9 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) {
function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
// top peers by prefixes received (eBGP only)
const topPeers = useMemo(() =>
[...sessions]
.filter(s => s.prefixesRx > 0)
.sort((a, b) => b.prefixesRx - a.prefixesRx)
(sessions.some(s => s.prefixesRx === null) ? [] : [...sessions])
.filter(s => s.prefixesRx !== null && s.prefixesRx > 0)
.sort((a, b) => (b.prefixesRx ?? 0) - (a.prefixesRx ?? 0))
.slice(0, 8),
[sessions]
)
@@ -448,8 +445,8 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
}, [sessions])
// total prefix stats
const totalRx = sessions.reduce((a, s) => a + s.prefixesRx, 0)
const totalActive = sessions.reduce((a, s) => a + s.prefixesActive, 0)
const totalRx = sumBgpCounters(sessions.map(s => s.prefixesRx))
const totalActive = sumBgpCounters(sessions.map(s => s.prefixesActive))
const ebgpSessions = sessions.filter(s => s.type === "eBGP").length
const ibgpSessions = sessions.filter(s => s.type === "iBGP").length
@@ -468,6 +465,7 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
{
id: "active",
label: "Активных маршрутов",
hint: totalActive === null ? "нет данных API" : undefined,
value: fmtNum(totalActive),
icon: <GitMergeIcon className="size-4" />,
iconClassName: "text-info",
@@ -494,7 +492,7 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
<OpsPanel title="Топ-8 пиров по полученным префиксам" contentClassName="pt-4 pb-4 px-5">
<div className="flex flex-col gap-3">
{topPeers.map((s, i) => {
const pct = (s.prefixesRx / maxRx) * 100
const pct = ((s.prefixesRx ?? 0) / maxRx) * 100
const ss = STATE_STYLE[s.state]
return (
<div key={s.id} className="flex items-center gap-3">
@@ -524,7 +522,7 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
)
})}
{topPeers.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">Нет данных о префиксах</p>
<p className="text-sm text-muted-foreground text-center py-4">Недостаточно данных о префиксах для сравнения</p>
)}
</div>
</OpsPanel>
@@ -543,7 +541,7 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
return (
<div key={state} className="flex items-center gap-2">
<span className={cn("size-2 rounded-full shrink-0", ss.dot)} />
<span className="text-xs w-24">{state}</span>
<span className="text-xs w-24">{ss.label}</span>
<div className="flex-1 h-2 rounded-full bg-muted overflow-hidden">
<div className={cn("h-full rounded-full", ss.dot)} style={{ width: `${pct}%`, opacity: 0.75 }} />
</div>
@@ -707,8 +705,8 @@ function BgpPageContent() {
}, [displayServers, allSessions, hasLoaded, isLive, unknownIds])
const established = sessions.filter(s => s.state === "Established").length
const notEstab = sessions.length - established
const totalRx = sessions.reduce((a, s) => a + s.prefixesRx, 0)
const notEstab = sessions.filter(s => s.state !== "Established" && s.state !== "Unknown").length
const totalRx = sumBgpCounters(sessions.map(s => s.prefixesRx))
return (
@@ -796,14 +794,14 @@ function BgpPageContent() {
{
id: "established",
label: "Established",
value: incomplete ? "—" : established,
value: incomplete || sessions.some(s => s.state === "Unknown") ? "—" : established,
icon: <CheckCircleIcon className="size-4" />,
iconClassName: "text-success",
},
{
id: "not-estab",
label: "Не установлено",
value: incomplete ? "—" : notEstab,
value: incomplete || sessions.some(s => s.state === "Unknown") ? "—" : notEstab,
icon: <AlertCircleIcon className="size-4" />,
iconClassName: notEstab > 0 ? "text-warning" : "text-muted-foreground",
variant: notEstab > 0 ? "warning" : "default",
@@ -826,7 +824,7 @@ function BgpPageContent() {
<span className="font-semibold">{notEstab} сессии</span> не в состоянии Established
</AlertTitle>
<AlertDescription>
Проверьте {sessions.filter(s => s.state !== "Established").map(s => s.peerIp).join(", ")}
Проверьте {sessions.filter(s => s.state !== "Established" && s.state !== "Unknown").map(s => s.peerIp).join(", ")}
</AlertDescription>
</Alert>
)}