fix(ui): ограничить граф OSPF с сохранением полного контекста

This commit is contained in:
Denozordec committed 2026-10-04 17:25:55 +07:00
1 parent 1a82c1b41f
commit 8d86c5cbb0
10 files changed
+194 -13

No files matched your search

+15 -9
View File
@@ -1,5 +1,6 @@
"use client"
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"
@@ -377,6 +378,7 @@ function Chip({ children, color }: { children: React.ReactNode; color?: string }
// ─── topology SVG ─────────────────────────────────────────────────────────────
interface TopologyGraphProps {
nodeStates?: ReadonlyMap<string, ReturnType<typeof ospfNodeState>>
graphNodes: GraphNode[]
graphEdges: GraphEdge[]
routerIds: Record<string, string>
@@ -387,7 +389,7 @@ interface TopologyGraphProps {
onNodeClick: (id: string | null) => void
}
function TopologyGraph({ graphNodes, graphEdges, highlightId, selectedId, showDots, onNodeClick }: TopologyGraphProps) {
function TopologyGraph({ graphNodes, graphEdges, nodeStates, highlightId, selectedId, showDots, onNodeClick }: TopologyGraphProps) {
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
const svg = svgRef.current, viewport = svg?.parentElement
@@ -405,6 +407,7 @@ function TopologyGraph({ graphNodes, graphEdges, highlightId, selectedId, showDo
const nodeMap = Object.fromEntries(graphNodes.map(n => [n.id, n]))
function nodeState(id: string): keyof typeof NODE_THEME {
if (nodeStates?.has(id)) return nodeStates.get(id)!
const involved = graphEdges.filter(e => e.from === id || e.to === id)
return ospfNodeState(involved.map(edge => edge.state))
}
@@ -469,7 +472,7 @@ function TopologyGraph({ graphNodes, graphEdges, highlightId, selectedId, showDo
(e.from === highlightId && e.to === node.id) ||
(e.to === highlightId && e.from === node.id))
return (
<g role="button" tabIndex={0} aria-label={`Открыть узел ${node.label}`} aria-expanded={isSel} className="focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring" data-ospf-node={node.id} key={node.id} transform={`translate(${node.x},${node.y})`}
<g role="button" tabIndex={0} aria-label={`Открыть узел ${node.label}`} aria-expanded={isSel} className="focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring" data-ospf-node={node.id} data-ospf-state={state} key={node.id} transform={`translate(${node.x},${node.y})`}
style={{ cursor: "pointer", transition: showDots ? "opacity 0.2s" : "none" }}
opacity={dimmed ? 0.25 : 1}
onClick={e => { e.stopPropagation(); onNodeClick(isSel ? null : node.id) }}
@@ -896,7 +899,7 @@ function NeighborsTab({
const reducedMotion = usePrefersReducedMotion()
function closeNode() {
const previous = selectedId; onSelect(null)
if (previous) requestAnimationFrame(() => document.querySelector<SVGGElement>(`[data-ospf-node="${CSS.escape(previous)}"]`)?.focus())
if (previous) requestAnimationFrame(() => (document.querySelector<SVGGElement>(`[data-ospf-node="${CSS.escape(previous)}"]`) ?? document.querySelector<HTMLInputElement>('[aria-label="Найти узел топологии"]'))?.focus())
}
const [showDots, setShowDots] = useState(true)
@@ -906,7 +909,7 @@ function NeighborsTab({
return (
<div className="flex flex-col gap-5">
<KpiStatGrid
gridClassName="grid-cols-2 md:grid-cols-3"
gridClassName="grid-cols-1 sm:grid-cols-3"
aria-label="Сводка соседей OSPF"
items={[
{
@@ -937,11 +940,13 @@ function NeighborsTab({
{selectedId && !graphUnknown && !graphNodes.some(node => node.id === selectedId) && <Alert variant="warning"><AlertTitle>Узел OSPF недоступен</AlertTitle><AlertDescription>Узел из ссылки отсутствует в текущем срезе.<Button type="button" size="sm" variant="outline" onClick={() => onSelect(null)}>Закрыть узел OSPF</Button></AlertDescription></Alert>}
{graphUnknown && <p role="status" className="text-sm text-muted-foreground">Топология OSPF неизвестна до успешного опроса всех устройств.</p>}
{!graphUnknown && graphNodes.length > 0 && (
<div className="flex min-w-0 flex-col rounded-xl overflow-hidden border border-border lg:h-[360px] lg:flex-row">
<OspfTopologyScope nodes={graphNodes} edges={graphEdges} selectedId={selectedId} onSelect={onSelect} width={TOPO_W} height={TOPO_H}>
{view => <div className="flex min-w-0 flex-col rounded-xl overflow-hidden border border-border lg:h-[360px] lg:flex-row">
<div className="relative min-w-0 overflow-x-auto lg:flex-1" style={{ background: "var(--background)", minWidth: 0 }}>
<TopologyGraph
graphNodes={graphNodes}
graphEdges={graphEdges}
graphNodes={view.nodes}
graphEdges={view.edges}
nodeStates={view.nodeStates}
routerIds={routerIds}
neighbors={neighbors}
highlightId={highlightId}
@@ -952,7 +957,7 @@ function NeighborsTab({
<button type="button" aria-pressed={showDots && !reducedMotion} disabled={reducedMotion} title={reducedMotion ? "Анимация отключена настройками устройства" : undefined}
onClick={() => setShowDots(v => !v)}
className={cn(
"absolute top-3 right-3 flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-[11px] font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
"absolute top-3 right-3 flex min-h-11 items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-[11px] font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
showDots
? "bg-background border-border text-foreground hover:bg-muted"
: "bg-background border-border text-muted-foreground hover:bg-muted",
@@ -976,7 +981,8 @@ function NeighborsTab({
onClose={closeNode}
/>
)}
</div>
</div>}
</OspfTopologyScope>
)}
{!graphUnknown && graphNodes.length > 0 && <div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted-foreground" aria-label="Легенда OSPF">