feat(bgp, vxlan, ospf): enhance server data handling and introduce new routes
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-test (push) Successful in 1m40s
Docker images / frontend-image (push) Successful in 2m50s
Docker images / updater-image (push) Successful in 42s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-test (push) Successful in 1m40s
Docker images / frontend-image (push) Successful in 2m50s
Docker images / updater-image (push) Successful in 42s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
- Added support for fetching and displaying server data in BGP, VXLAN, and OSPF pages, improving the overall user experience. - Introduced new backend routes for OSPF and VXLAN, allowing for better data management and retrieval. - Implemented mapping functions for backend server data to frontend types, ensuring consistency across components. - Enhanced the sidebar to display counts for BGP sessions, VXLAN tunnels, and containers, providing users with quick insights into their network status. - Updated tests to cover new functionalities and ensure reliability. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+130
-42
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Fragment, useState, useMemo, useEffect } from "react"
|
||||
import { useState, useMemo, useEffect } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { DataPageToolbar } from "@/components/data-page-toolbar"
|
||||
import { BgpSessionsDataGrid } from "@/components/data-grids/bgp-sessions-data-grid"
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
} from "lucide-react"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { servers as mockServers, type Server } from "@/lib/data"
|
||||
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
|
||||
import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-rail"
|
||||
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -233,6 +236,37 @@ interface BackendBgpSession {
|
||||
capabilities: string[]; lastError: string | null
|
||||
}
|
||||
|
||||
interface BackendServer {
|
||||
id: number
|
||||
name: string
|
||||
host: string
|
||||
type?: Server["type"]
|
||||
site?: string
|
||||
country: string
|
||||
asn?: string
|
||||
enabled: boolean
|
||||
status?: Server["status"]
|
||||
latency?: number | null
|
||||
}
|
||||
|
||||
function mapBackendServer(s: BackendServer): Server {
|
||||
return {
|
||||
id: String(s.id),
|
||||
name: s.name || s.host,
|
||||
host: s.host,
|
||||
model: "—",
|
||||
os: "—",
|
||||
site: s.site ?? "",
|
||||
country: s.country || "UN",
|
||||
asn: s.asn ?? "",
|
||||
type: s.type ?? "exit-node",
|
||||
enabled: s.enabled,
|
||||
status: s.status ?? "online",
|
||||
latency: s.latency ?? null,
|
||||
sessions: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function backendToFrontend(b: BackendBgpSession): BgpSession {
|
||||
return {
|
||||
id: `${b.serverId}-${b.id}`,
|
||||
@@ -623,27 +657,40 @@ const TABS: Array<{ id: BgpTab; label: string; icon: React.ReactNode }> = [
|
||||
|
||||
export default function BgpPage() {
|
||||
const [activeTab, setActiveTab] = useState<BgpTab>("sessions")
|
||||
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
|
||||
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
|
||||
const [liveSessions, setLiveSessions] = useState<BgpSession[]>([])
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fetchedAt, setFetchedAt] = useState<Date | null>(null)
|
||||
const [liveError, setLiveError] = useState<string | null>(null)
|
||||
const [fetchTick, setFetchTick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) return
|
||||
if (!isLive) {
|
||||
queueMicrotask(() => {
|
||||
setLiveSessions([])
|
||||
setLiveServers([])
|
||||
setLiveError(null)
|
||||
})
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return
|
||||
setLoading(true)
|
||||
setLiveError(null)
|
||||
void requestJson<BackendBgpSession[]>(backendUrl, "/api/bgp/sessions")
|
||||
.then(data => {
|
||||
void Promise.all([
|
||||
requestJson<BackendBgpSession[]>(backendUrl, "/api/bgp/sessions"),
|
||||
requestJson<BackendServer[]>(backendUrl, "/api/servers"),
|
||||
])
|
||||
.then(([data, servers]) => {
|
||||
if (cancelled) return
|
||||
setLiveSessions(data.map(backendToFrontend))
|
||||
setLiveServers(servers.filter((s) => s.enabled).map(mapBackendServer))
|
||||
setFetchedAt(new Date())
|
||||
setLoading(false)
|
||||
})
|
||||
@@ -656,50 +703,87 @@ export default function BgpPage() {
|
||||
return () => { cancelled = true }
|
||||
}, [isLive, backendUrl, fetchTick])
|
||||
|
||||
// Use live or mock data for all tabs and KPI
|
||||
const sessions = isLive ? liveSessions : SESSIONS
|
||||
const allSessions = isLive ? liveSessions : SESSIONS
|
||||
const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled)
|
||||
|
||||
const effectiveServerId =
|
||||
selectedServerId === ALL_SERVERS_ID || displayServers.some((s) => s.id === selectedServerId)
|
||||
? selectedServerId
|
||||
: ALL_SERVERS_ID
|
||||
|
||||
const sessions = useMemo(() => {
|
||||
if (effectiveServerId === ALL_SERVERS_ID) return allSessions
|
||||
return allSessions.filter((s) => s.serverId === effectiveServerId)
|
||||
}, [allSessions, effectiveServerId])
|
||||
|
||||
const railItems = useMemo<ServerTileItem[]>(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const s of allSessions) {
|
||||
counts.set(s.serverId, (counts.get(s.serverId) ?? 0) + 1)
|
||||
}
|
||||
return displayServers.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
host: s.host,
|
||||
site: s.site,
|
||||
country: s.country,
|
||||
status: s.status,
|
||||
type: s.type,
|
||||
count: counts.get(s.id) ?? 0,
|
||||
enabled: s.enabled,
|
||||
title: [s.name, s.host, s.asn].filter(Boolean).join(" · "),
|
||||
}))
|
||||
}, [displayServers, allSessions])
|
||||
|
||||
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 serverCount = useMemo(
|
||||
() => new Set(liveSessions.map(s => s.serverId)).size,
|
||||
[liveSessions],
|
||||
() => new Set(sessions.map(s => s.serverId)).size,
|
||||
[sessions],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "BGP" }]}
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => setFetchTick(t => t + 1)}>
|
||||
<RefreshCwIcon className={cn("size-4", loading && "animate-spin")} />
|
||||
Обновить
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* tab bar */}
|
||||
<div className="border-b bg-background shrink-0">
|
||||
<div className="flex items-center px-6">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} onClick={() => setActiveTab(t.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||
activeTab === t.id
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border",
|
||||
)}>
|
||||
{t.icon}{t.label}
|
||||
</button>
|
||||
))}
|
||||
<ServerRailLayout
|
||||
items={railItems}
|
||||
selectedId={effectiveServerId}
|
||||
onSelect={setSelectedServerId}
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "BGP" }]}
|
||||
actions={
|
||||
<>
|
||||
<ServerRailMobileButton />
|
||||
<Button variant="outline" size="sm" onClick={() => setFetchTick(t => t + 1)}>
|
||||
<RefreshCwIcon className={cn("size-4", loading && "animate-spin")} />
|
||||
Обновить
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
banner={
|
||||
<div className="border-b bg-background shrink-0">
|
||||
<div className="flex items-center px-6">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} onClick={() => setActiveTab(t.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||
activeTab === t.id
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border",
|
||||
)}>
|
||||
{t.icon}{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
|
||||
{/* data source banner */}
|
||||
@@ -729,11 +813,16 @@ export default function BgpPage() {
|
||||
<AlertDescription className="text-xs">Ошибка загрузки: {liveError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{isLive && !loading && liveSessions.length === 0 && !liveError && fetchedAt && (
|
||||
{isLive && !loading && allSessions.length === 0 && !liveError && fetchedAt && (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
BGP не настроен ни на одном сервере
|
||||
</div>
|
||||
)}
|
||||
{isLive && !loading && allSessions.length > 0 && sessions.length === 0 && !liveError && (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
На выбранном сервере нет BGP-сессий
|
||||
</div>
|
||||
)}
|
||||
{mode === "mock" && (
|
||||
<span className="inline-flex w-fit items-center gap-1.5 rounded-full border border-border bg-muted/40 px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||
Моковые данные
|
||||
@@ -794,7 +883,6 @@ export default function BgpPage() {
|
||||
{activeTab === "analytics" && <AnalyticsTab sessions={sessions} />}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ServerRailLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user