fix(api): показывать подтверждённые метрики OSPF
This commit is contained in:
1 parent
e3791f3496
commit
af05ad5cda
13 files changed
+120
-55
No files matched your search
+20
-40
@@ -46,13 +46,13 @@ interface OspfItem {
|
||||
serverId: string
|
||||
interfaceName: string
|
||||
area: string
|
||||
cost: number
|
||||
active: boolean
|
||||
cost: number | null
|
||||
active: boolean | null
|
||||
dataUnknown?: boolean
|
||||
type?: string
|
||||
useBfd?: boolean
|
||||
helloInterval?: number
|
||||
deadInterval?: number
|
||||
type?: string | null
|
||||
useBfd?: boolean | null
|
||||
helloInterval?: number | null
|
||||
deadInterval?: number | null
|
||||
}
|
||||
|
||||
interface OspfNeighbor {
|
||||
@@ -75,7 +75,7 @@ interface OspfRoute {
|
||||
id: string
|
||||
destination: string
|
||||
type: "O" | "O IA" | "O E1" | "O E2"
|
||||
cost: number
|
||||
cost: number | null
|
||||
nextHop: string
|
||||
via: string
|
||||
serverId: string
|
||||
@@ -106,31 +106,10 @@ interface BfdSession {
|
||||
|
||||
// ─── backend types ────────────────────────────────────────────────────────────
|
||||
|
||||
interface BackendNeighbor {
|
||||
id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
|
||||
address: string; routerId: string; instance: string; area: string; areaId: string
|
||||
interface: string; state: string; uptime: string | null; stateChanges: number | null; priority: number | null
|
||||
}
|
||||
|
||||
interface BackendInterface {
|
||||
id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
|
||||
instance: string; area: string; areaId: string; interface: string; cost: number
|
||||
type: string; disabled: boolean; inactive: boolean; priority: number
|
||||
helloInterval: number; deadInterval: number; useBfd: boolean
|
||||
}
|
||||
|
||||
|
||||
interface BackendBfdSession {
|
||||
id: string; serverId: number; serverName: string; serverSite: string; serverCountry: string
|
||||
localAddr: string; remoteAddr: string; interface: string; state: string; uptime: string | null
|
||||
multihop: boolean; multiplier: number | null; txInterval: number | null; rxInterval: number | null; holdTime: number | null
|
||||
packetsRx: number | null; packetsTx: number | null; stateChanges: number | null
|
||||
}
|
||||
|
||||
interface BackendOspfRoute {
|
||||
id: string; serverId: number; serverName: string; serverSite: string
|
||||
destination: string; type: OspfRoute["type"]; cost: number; nextHop: string; via: string; area: string
|
||||
}
|
||||
type BackendNeighbor = OspfCatalog["neighbors"][number]
|
||||
type BackendInterface = OspfCatalog["interfaces"][number]
|
||||
type BackendBfdSession = OspfCatalog["bfdSessions"][number]
|
||||
type BackendOspfRoute = OspfCatalog["routes"][number]
|
||||
|
||||
|
||||
function isRefInterfaceName(name: string): boolean {
|
||||
@@ -168,7 +147,7 @@ function backendToItem(b: BackendInterface): OspfItem {
|
||||
interfaceName: b.interface,
|
||||
area: b.areaId,
|
||||
cost: b.cost,
|
||||
active: !b.disabled && !b.inactive,
|
||||
active: b.disabled === true || b.inactive === true ? false : b.disabled === false && b.inactive === false ? true : null,
|
||||
type: b.type,
|
||||
useBfd: b.useBfd,
|
||||
helloInterval: b.helloInterval,
|
||||
@@ -637,7 +616,7 @@ function NodeDetailPanel({
|
||||
<span className="text-[10px] font-mono text-[#475569] shrink-0">
|
||||
{iface.area === "0.0.0.0" ? "backbone" : `area ${iface.area}`}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono shrink-0" style={{ color: "#0ea5e9" }}>cost {iface.cost}</span>
|
||||
<span className="text-[10px] font-mono shrink-0" style={{ color: "#0ea5e9" }}>cost {iface.cost ?? "—"}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -696,7 +675,7 @@ function InterfacesTab({
|
||||
return Object.values(byRouter).map(r => ({
|
||||
...r,
|
||||
areas: Object.entries(r.areas)
|
||||
.map(([area, areaItems]) => ({ area, items: [...areaItems].sort((a, b) => a.cost - b.cost) }))
|
||||
.map(([area, areaItems]) => ({ area, items: [...areaItems].sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity)) }))
|
||||
.sort((a, b) => a.area.localeCompare(b.area)),
|
||||
}))
|
||||
}, [items])
|
||||
@@ -765,7 +744,7 @@ function InterfacesTab({
|
||||
function onDrop(routerKey: string, area: string, targetKey: string) {
|
||||
if (!dragging || dragging === targetKey) { setDragging(null); setDragOver(null); return }
|
||||
setItems(prev => {
|
||||
const areaItems = prev.filter(i => i.routerKey === routerKey && i.area === area).sort((a, b) => a.cost - b.cost)
|
||||
const areaItems = prev.filter(i => i.routerKey === routerKey && i.area === area).sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity))
|
||||
const fromIdx = areaItems.findIndex(i => i.key === dragging)
|
||||
const toIdx = areaItems.findIndex(i => i.key === targetKey)
|
||||
if (fromIdx < 0 || toIdx < 0) return prev
|
||||
@@ -785,6 +764,7 @@ function InterfacesTab({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{isLive && filterServerId !== ALL_SERVERS_ID && !dataReady && <p role="status" className="text-xs text-muted-foreground">Оптимизация недоступна до полного успешного опроса и подтверждения costs/состояний интерфейсов с правом записи.</p>}
|
||||
{operationError && <Alert variant="warning"><AlertCircleIcon /><AlertTitle>Оптимизация OSPF не подтверждена</AlertTitle><AlertDescription>{operationError}</AlertDescription></Alert>}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button variant="outline" size="sm" disabled={optimizing} onClick={() => setItems(initialItems)}>
|
||||
@@ -856,7 +836,7 @@ function InterfacesTab({
|
||||
<GripVerticalIcon className={cn("size-3.5 shrink-0", isLive ? "text-muted-foreground/10" : "text-muted-foreground/30")} />
|
||||
<span className={cn("inline-block size-1.5 rounded-full shrink-0", !item.dataUnknown && item.active ? "bg-[var(--status-online)]" : "bg-muted-foreground/40")} />
|
||||
<code className="text-xs font-mono flex-1 min-w-0 truncate">{item.interfaceName}</code>
|
||||
{item.dataUnknown && <span className="text-xs text-muted-foreground">Неизвестно</span>}
|
||||
{(item.dataUnknown || item.active === null) && <span className="text-xs text-muted-foreground">Неизвестно</span>}
|
||||
{item.type && item.type !== "broadcast" && (
|
||||
<Chip color="bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20">{item.type}</Chip>
|
||||
)}
|
||||
@@ -869,7 +849,7 @@ function InterfacesTab({
|
||||
: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
|
||||
}>opt {hint.optimalCost}</Chip>
|
||||
)}
|
||||
<Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cur {item.cost}</Chip>
|
||||
<Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cur {item.cost ?? "—"}</Chip>
|
||||
{liveOptimal !== undefined && !item.dataUnknown && <Chip color={costDiffers
|
||||
? "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
|
||||
: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
|
||||
@@ -1193,7 +1173,7 @@ function OspfPageContent() {
|
||||
|
||||
const unknownIds = useMemo(() => new Set(liveError ? liveServers.map(s => s.id) : (liveData?.failures ?? []).map(f => f.serverId)), [liveError, liveServers, liveData])
|
||||
const incomplete = isLive && (!liveData || Boolean(liveError) || (filterServerId === ALL_SERVERS_ID ? unknownIds.size > 0 : unknownIds.has(Number(filterServerId))))
|
||||
const dataReady = !isLive || Boolean(liveData && !liveError && !loading && !writeBusy && !liveData.failures.length && liveServers.some(s => String(s.id) === filterServerId) && can("mm:servers:write"))
|
||||
const dataReady = !isLive || Boolean(liveData && !liveError && !loading && !writeBusy && !liveData.failures.length && liveServers.some(s => String(s.id) === filterServerId) && can("mm:servers:write") && liveData.interfaces.filter(row => String(row.serverId) === filterServerId).every(row => row.cost !== null && row.cost >= 1 && row.disabled !== null && row.inactive !== null))
|
||||
|
||||
// Derive frontend types from backend data or use mocks
|
||||
const { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions, routes } = useMemo(() => {
|
||||
@@ -1211,7 +1191,7 @@ function OspfPageContent() {
|
||||
// Build routerIds from instances
|
||||
const routerIds: Record<string, string> = {}
|
||||
for (const inst of liveData.instances) {
|
||||
if (!routerIds[String(inst.serverId)]) {
|
||||
if (inst.routerId && !routerIds[String(inst.serverId)]) {
|
||||
routerIds[String(inst.serverId)] = inst.routerId
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user