fix(ospf): показать конфликт состояний OSPF и BFD

This commit is contained in:
Denozordec committed 2026-10-11 22:42:14 +07:00
1 parent 4d3cb659c1
commit 12ead0b6c2
15 files changed
+6321 -18

No files matched your search

+7 -3
View File
@@ -5,7 +5,7 @@ import { OspfTopologyScope } from "@/components/ospf-topology-scope"
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion"
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 { normalizeOspfState, normalizeBfdState, ospfNodeState, ospfBfdConflicts, type OspfNeighborState, type BfdState } from "@/lib/ospf-state"
import { useMemo, useState, useEffect, useRef, Suspense } from "react"
import { useSearchParams } from "next/navigation"
import { readOspfUrl, patchOspfUrl, type OspfTab } from "@/lib/ospf-url-state"
@@ -62,6 +62,7 @@ interface OspfItem {
}
interface OspfNeighbor {
neighborAddress?: string
id: string
localRouter: string
localLabel: string
@@ -136,6 +137,7 @@ function backendToNeighbor(b: BackendNeighbor, ifaceMap: Map<string, number | nu
remoteRouter: serverByRouterId[b.routerId] ?? b.routerId,
remoteLabel: labels[serverByRouterId[b.routerId]] ?? b.routerId,
remoteRouterId: b.routerId,
neighborAddress: b.address,
area: b.areaId !== b.area ? `${b.area} (${b.areaId})` : b.area,
state,
cost: ifaceMap.get(costKey) ?? null,
@@ -1144,9 +1146,9 @@ function OspfRoute() {
const href = patchOspfUrl(new URLSearchParams(window.location.search), values)
if (href !== window.location.pathname + window.location.search) (replace ? window.history.replaceState : window.history.pushState).call(window.history, null, "", href)
}
return <OspfPageContent key={JSON.stringify([mode, backendUrl])} filterServerId={state.server} activeTab={state.tab} contextError={state.error} search={state.search} selectedNodeId={state.node} onSearch={value => navigate({ q: value }, true)} onSelectNode={node => navigate({ node })} onSelectServer={id => navigate({ server: id === ALL_SERVERS_ID ? null : id, node: null })} onSelectTab={tab => navigate({ tab: tab === "interfaces" ? null : tab })} onResetContext={() => navigate({ server: null, tab: null, q: null, node: null })} />
return <OspfPageContent key={JSON.stringify([mode, backendUrl])} filterServerId={state.server} activeTab={state.tab} contextError={state.error} search={state.search} selectedNodeId={state.node} onSearch={value => navigate({ q: value }, true)} onSelectNode={node => navigate({ node })} onSelectServer={id => navigate({ server: id === ALL_SERVERS_ID ? null : id, node: null })} onSelectTab={tab => navigate({ tab: tab === "interfaces" ? null : tab })} onInspectBfd={() => navigate({ tab: "bfd", q: null })} onResetContext={() => navigate({ server: null, tab: null, q: null, node: null })} />
}
function OspfPageContent({ search, selectedNodeId, onSearch, onSelectNode, filterServerId, activeTab, contextError, onSelectServer, onSelectTab, onResetContext }: { search: string; selectedNodeId: string | null; onSearch: (value: string) => void; onSelectNode: (id: string | null) => void; filterServerId: string; activeTab: OspfTab; contextError: string | null; onSelectServer: (id: string) => void; onSelectTab: (tab: OspfTab) => void; onResetContext: () => void }) {
function OspfPageContent({ search, selectedNodeId, onSearch, onSelectNode, filterServerId, activeTab, contextError, onSelectServer, onSelectTab, onInspectBfd, onResetContext }: { search: string; selectedNodeId: string | null; onSearch: (value: string) => void; onSelectNode: (id: string | null) => void; filterServerId: string; activeTab: OspfTab; contextError: string | null; onSelectServer: (id: string) => void; onSelectTab: (tab: OspfTab) => void; onInspectBfd: () => void; onResetContext: () => void }) {
const { mode, backendUrl } = useDataSource()
const isLive = mode === "live"
@@ -1272,6 +1274,7 @@ function OspfPageContent({ search, selectedNodeId, onSearch, onSelectNode, filte
const scopedNeighbors = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? neighbors : neighbors.filter(n => n.localRouter === filterServerId)
const scopedBfdSessions = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? bfdSessions : bfdSessions.filter(b => b.serverId === filterServerId)
const scopedRoutes = unavailable ? [] : filterServerId === ALL_SERVERS_ID ? routes : routes.filter(r => r.serverId === filterServerId)
const bfdConflicts = ospfBfdConflicts(scopedNeighbors, scopedItems, scopedBfdSessions)
const query = search.trim().toLocaleLowerCase("ru")
const matches = (...fields: (string | number | null | undefined)[]) => !query || fields.some(field => String(field ?? "").toLocaleLowerCase("ru").includes(query))
const displayItems = scopedItems.filter(i => matches(i.routerLabel, i.interfaceName, i.area, i.type))
@@ -1350,6 +1353,7 @@ function OspfPageContent({ search, selectedNodeId, onSearch, onSelectNode, filte
<div className="flex flex-col gap-5">
{unavailable && <Alert variant="warning"><AlertCircleIcon /><AlertTitle>Контекст OSPF недоступен</AlertTitle><AlertDescription>{contextError ?? `Сервер ${filterServerId} отсутствует среди доступных устройств.`}<Button type="button" variant="outline" size="sm" disabled={writeBusy} onClick={onResetContext}>Сбросить контекст OSPF</Button></AlertDescription></Alert>}
{bfdConflicts.length > 0 && <Alert variant="warning"><AlertCircleIcon /><AlertTitle>OSPF Full при недоступном BFD</AlertTitle><AlertDescription>Последний опрос: {bfdConflicts.map(n => `${n.localLabel} · ${n.localIface}`).join("; ")} — OSPF Full, BFD не Up. Проверьте канал: Full не подтверждает работу BFD.<Button type="button" variant="outline" size="sm" disabled={writeBusy} onClick={() => { if (!operationBusy.current) onInspectBfd() }}>Проверить BFD</Button></AlertDescription></Alert>}
<DataPageToolbar search={search} onSearchChange={onSearch} searchPlaceholder="Поиск в текущем разделе OSPF…" countLabel={viewIncomplete ? "Данные неизвестны" : `${visibleCount} из ${scopeCount}`} actions={search ? <Button type="button" variant="ghost" size="sm" onClick={() => onSearch("")}>Сбросить поиск</Button> : undefined} />
{/* data source banner */}
{isLive && loading && (