fix(ui): объединить параллельные связи OSPF в читаемый граф

This commit is contained in:
Denozordec committed 2026-10-04 17:37:42 +07:00
1 parent 086f118204
commit 9f74cfebfe
8 files changed
+43 -13

No files matched your search

+21 -8
View File
@@ -406,6 +406,14 @@ function TopologyGraph({ graphNodes, graphEdges, nodeStates, highlightId, select
return () => observer.disconnect()
}, [graphNodes, selectedId])
const nodeMap = Object.fromEntries(graphNodes.map(n => [n.id, n]))
const visualLinks = useMemo(() => {
const groups = new Map<string, GraphEdge[]>()
for (const edge of graphEdges) {
const key = JSON.stringify([edge.from, edge.to].sort())
const links = groups.get(key) ?? []; links.push(edge); groups.set(key, links)
}
return [...groups.values()]
}, [graphEdges])
function nodeState(id: string): keyof typeof NODE_THEME {
if (nodeStates?.has(id)) return nodeStates.get(id)!
@@ -427,34 +435,39 @@ function TopologyGraph({ graphNodes, graphEdges, nodeStates, highlightId, select
<rect width={TOPO_W} height={TOPO_H} fill="var(--background)" />
<rect width={TOPO_W} height={TOPO_H} fill="url(#ospf-dots)" />
{graphEdges.map((e, i) => {
{visualLinks.map((links, i) => {
const e = links[0]
const mixed = links.some(link => link.state !== e.state)
const grouped = links.length > 1
const badgeWidth = grouped ? 90 : 30
const a = nodeMap[e.from]
const b = nodeMap[e.to]
if (!a || !b) return null
const color = EDGE_COLOR[e.state] ?? "var(--muted-foreground)"
const isDashed = e.state !== "Full"
const color = mixed ? "var(--muted-foreground)" : EDGE_COLOR[e.state] ?? "var(--muted-foreground)"
const isDashed = mixed || e.state !== "Full"
const active = highlightId === e.from || highlightId === e.to
|| selectedId === e.from || selectedId === e.to
const opacity = active ? 1 : 0.38
const opacity = active || grouped ? 1 : 0.38
const strokeW = active ? 2.4 : 1.4
const mx = (a.x + b.x) / 2
const my = (a.y + b.y) / 2
return (
<g key={i} style={{ transition: showDots ? "opacity 0.2s" : "none" }} opacity={opacity}>
<g key={i} role={grouped ? "img" : undefined} data-ospf-links={links.length} aria-label={grouped ? `Параллельные связи: ${links.length}. ${links.map(link => `${link.state}, cost ${link.cost ?? "—"}`).join("; ")}` : undefined} style={{ transition: showDots ? "opacity 0.2s" : "none" }} opacity={opacity}>
{grouped && <title>{links.map(link => `${link.state}, cost ${link.cost ?? "—"}`).join("; ")}</title>}
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y}
stroke={color} strokeWidth={strokeW}
strokeDasharray={isDashed ? "7 4" : "none"} />
{showDots && e.state === "Full" && (
{showDots && !mixed && e.state === "Full" && (
<circle r="3.5" fill={color} opacity="0.9">
<animateMotion dur={`${2.2 + (i % 5) * 0.38}s`} repeatCount="indefinite"
path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
</circle>
)}
<g transform={`translate(${mx},${my})`}>
<rect x="-15" y="-10" width="30" height="18" rx="4"
<rect x={-badgeWidth / 2} y="-10" width={badgeWidth} height="18" rx="4"
fill="var(--background)" stroke={color} strokeWidth="0.8" opacity="0.95" />
<text textAnchor="middle" y="4" fontSize="12" fontWeight="700"
fill={color} fontFamily="ui-monospace,monospace">{e.cost ?? "—"}</text>
fill={color} fontFamily="ui-monospace,monospace">{grouped ? `${links.length} ${links.length < 5 ? "связи" : "связей"}` : e.cost ?? "—"}</text>
</g>
</g>
)