fix(ui): связывать OSPF узлы по подтверждённым ID

This commit is contained in:
Denozordec committed 2026-09-30 20:19:37 +07:00
1 parent 7a68ae8184
commit 259c5c3014
16 files changed
+123 -41

No files matched your search

+20 -25
View File
@@ -1,5 +1,6 @@
"use client"
import { deriveOspfRouterIdentity } from "@/lib/ospf-router-identity"
import { buildOspfCostIndex, ospfCostKey } from "@/lib/ospf-cost"
import { normalizeOspfState, normalizeBfdState, ospfNodeState, type OspfNeighborState, type BfdState } from "@/lib/ospf-state"
import { useMemo, useState, useEffect, useRef, Suspense } from "react"
@@ -76,7 +77,7 @@ interface OspfNeighbor {
interface OspfRoute {
id: string
destination: string
type: "O" | "O IA" | "O E1" | "O E2"
type: "O" | "O IA" | "O E1" | "O E2" | "Unknown"
cost: number | null
nextHop: string
via: string
@@ -97,7 +98,7 @@ interface BfdSession {
multiplier: number | null
iface: string
uptime: string | null
multihop: boolean
multihop: boolean | null
rxInterval: number | null // ms
holdTime: number | null // ms
packetsRx: number | null
@@ -121,7 +122,7 @@ function isRefInterfaceName(name: string): boolean {
// ─── backend → frontend mappers ───────────────────────────────────────────────
function backendToNeighbor(b: BackendNeighbor, ifaceMap: Map<string, number | null>): OspfNeighbor {
function backendToNeighbor(b: BackendNeighbor, ifaceMap: Map<string, number | null>, serverByRouterId: Record<string, string>, labels: Record<string, string>): OspfNeighbor {
const state = normalizeOspfState(b.state)
const costKey = ospfCostKey(b.serverId, b.interface, b.areaId)
return {
@@ -129,8 +130,8 @@ function backendToNeighbor(b: BackendNeighbor, ifaceMap: Map<string, number | nu
localRouter: String(b.serverId),
localLabel: b.serverName,
localIface: b.interface,
remoteRouter: b.routerId,
remoteLabel: b.routerId, // no server name for remote peers
remoteRouter: serverByRouterId[b.routerId] ?? b.routerId,
remoteLabel: labels[serverByRouterId[b.routerId]] ?? b.routerId,
remoteRouterId: b.routerId,
area: b.areaId !== b.area ? `${b.area} (${b.areaId})` : b.area,
state,
@@ -180,8 +181,8 @@ function backendToBfdSession(b: BackendBfdSession): BfdSession {
}
function backendToRoute(b: BackendOspfRoute): OspfRoute {
const allowed: OspfRoute["type"][] = ["O", "O IA", "O E1", "O E2"]
const type = allowed.includes(b.type) ? b.type : "O"
const allowed: OspfRoute["type"][] = ["O", "O IA", "O E1", "O E2", "Unknown"]
const type = allowed.includes(b.type) ? b.type : "Unknown"
return {
id: `${b.serverId}-${b.id}`,
destination: b.destination,
@@ -320,9 +321,9 @@ function buildLiveGraph(
// Collect unique remote peers (that are NOT already local)
const remoteMap = new Map<string, { label: string }>()
for (const n of neighbors) {
const remId = `remote-${n.remoteRouterId}`
const remId = n.remoteRouter !== n.remoteRouterId ? n.remoteRouter : `remote-${n.remoteRouterId}`
if (!localMap.has(n.remoteRouter) && !remoteMap.has(remId)) {
remoteMap.set(remId, { label: n.remoteRouterId })
remoteMap.set(remId, { label: n.remoteLabel })
}
}
@@ -342,7 +343,7 @@ function buildLiveGraph(
const edges: GraphEdge[] = neighbors.map(n => ({
from: n.localRouter,
to: localMap.has(n.remoteRouter) ? n.remoteRouter : `remote-${n.remoteRouterId}`,
to: n.remoteRouter !== n.remoteRouterId || localMap.has(n.remoteRouter) ? n.remoteRouter : `remote-${n.remoteRouterId}`,
state: n.state,
cost: n.cost,
}))
@@ -460,7 +461,7 @@ function TopologyGraph({ graphNodes, graphEdges, highlightId, selectedId, showDo
(e.from === highlightId && e.to === node.id) ||
(e.to === highlightId && e.from === node.id))
return (
<g key={node.id} transform={`translate(${node.x},${node.y})`}
<g data-ospf-node={node.id} key={node.id} transform={`translate(${node.x},${node.y})`}
style={{ cursor: "pointer", transition: "opacity 0.2s" }}
opacity={dimmed ? 0.25 : 1}
onClick={e => { e.stopPropagation(); onNodeClick(isSel ? null : node.id) }}>
@@ -537,12 +538,12 @@ function NodeDetailPanel({
const node = graphNodes.find(n => n.id === nodeId)
if (!node) return null
const routerId = routerIds[nodeId] ?? "—"
const adjacencies = neighbors.filter(n => n.localRouter === nodeId || n.remoteRouter === nodeId)
const routerId = routerIds[nodeId] ?? (nodeId.startsWith("remote-") ? nodeId.slice(7) : "—")
const adjacencies = neighbors.filter(n => n.localRouter === nodeId || n.remoteRouter === nodeId || nodeId === `remote-${n.remoteRouterId}`)
.map(n => {
if (n.localRouter === nodeId)
return { iface: n.localIface, peerLabel: n.remoteLabel, peerId: n.remoteRouterId, state: n.state, cost: n.cost, uptime: n.uptime }
return { iface: n.localIface, peerLabel: n.localLabel, peerId: routerId, state: n.state, cost: n.cost, uptime: n.uptime }
return { iface: n.localIface, peerLabel: n.localLabel, peerId: routerIds[n.localRouter] ?? "—", state: n.state, cost: n.cost, uptime: n.uptime }
})
const ifaces = items.filter(i => i.routerKey === nodeId)
const fullCnt = adjacencies.filter(a => a.state === "Full").length
@@ -1010,7 +1011,7 @@ function RoutesTab({ routes, hasSearch }: { routes: OspfRoute[]; hasSearch: bool
<span className="text-sm font-medium">{routes.some(r => r.dataUnknown) ? "Последний снимок: " : ""}{routes.length} маршрутов</span>
{typeCount.map(([type, count]) => (
<span key={type} className={cn("inline-flex items-center rounded border px-2 py-0.5 text-[11px] font-medium", routeTypeClass(type as OspfRoute["type"]))}>
{type}: {count}
{type === "Unknown" ? "Тип неизвестен" : type}: {count}
</span>
))}
</div>
@@ -1190,21 +1191,15 @@ function OspfPageContent({ search, selectedNodeId, onSearch, onSelectNode, filte
// Build interface→cost map for neighbor cost lookup
const ifaceMap = buildOspfCostIndex(liveData.interfaces)
const { routerIds, serverByRouterId } = deriveOspfRouterIdentity(liveData.instances)
const labels = Object.fromEntries(liveServers.map(server => [String(server.id), server.name]))
const items = liveData.interfaces
.map(b => ({ ...backendToItem(b), dataUnknown: unknownIds.has(b.serverId) }))
.filter((item) => !isRefInterfaceName(item.interfaceName))
const neighbors = liveData.neighbors.map(b => ({ ...backendToNeighbor(b, ifaceMap), dataUnknown: unknownIds.has(b.serverId) }))
const neighbors = liveData.neighbors.map(b => ({ ...backendToNeighbor(b, ifaceMap, serverByRouterId, labels), dataUnknown: unknownIds.has(b.serverId) }))
const bfdSessions = liveData.bfdSessions.map(b => ({ ...backendToBfdSession(b), dataUnknown: unknownIds.has(b.serverId) }))
const routes = liveData.routes.map(b => ({ ...backendToRoute(b), dataUnknown: unknownIds.has(b.serverId) }))
// Build routerIds from instances
const routerIds: Record<string, string> = {}
for (const inst of liveData.instances) {
if (inst.routerId && !routerIds[String(inst.serverId)]) {
routerIds[String(inst.serverId)] = inst.routerId
}
}
const { nodes: graphNodes, edges: graphEdges } = buildLiveGraph(neighbors)
return { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions, routes }
}
@@ -1228,7 +1223,7 @@ function OspfPageContent({ search, selectedNodeId, onSearch, onSelectNode, filte
bfdSessions: MOCK_BFD,
routes: MOCK_ROUTES,
}
}, [isLive, liveData, unknownIds])
}, [isLive, liveData, liveServers, unknownIds])
// ── server list for chips bar ────────────────────────────────────────────────
const ospfServers = useMemo((): OspfServerInfo[] => {