feat(topology): add network topology page; tighten config save check
- Interactive SRv6 topology canvas (@xyflow/react + d3-force auto-layout), custom nodes/edges, detail sheet, search and relayout controls - Strict success-flag validation on /api/config save responses
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
getSmoothStepPath,
|
||||
useReactFlow,
|
||||
type EdgeProps,
|
||||
type Edge,
|
||||
} from '@xyflow/react'
|
||||
import { formatNanoDelay } from '@/lib/format'
|
||||
|
||||
export interface LinkEdgeData {
|
||||
delay: number
|
||||
cost: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type LinkEdgeType = Edge<LinkEdgeData, 'link'>
|
||||
|
||||
function delayColor(delayNs: number): { stroke: string; badge: string } {
|
||||
const ms = delayNs / 1_000_000
|
||||
if (delayNs <= 0 || !Number.isFinite(delayNs)) {
|
||||
return { stroke: 'hsl(var(--muted-foreground))', badge: 'bg-muted text-muted-foreground' }
|
||||
}
|
||||
if (ms < 1) {
|
||||
return { stroke: '#10b981', badge: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400' }
|
||||
}
|
||||
if (ms < 20) {
|
||||
return { stroke: '#f59e0b', badge: 'bg-amber-500/15 text-amber-600 dark:text-amber-400' }
|
||||
}
|
||||
return { stroke: '#f43f5e', badge: 'bg-rose-500/15 text-rose-600 dark:text-rose-400' }
|
||||
}
|
||||
|
||||
function LinkEdgeComponent({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
selected,
|
||||
}: EdgeProps<LinkEdgeType>) {
|
||||
const { setEdges } = useReactFlow()
|
||||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius: 16,
|
||||
})
|
||||
|
||||
const delay = data?.delay ?? 0
|
||||
const { stroke, badge } = delayColor(delay)
|
||||
const showLabel = delay > 0 && Number.isFinite(delay)
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
style={{
|
||||
strokeWidth: selected ? 2.5 : 1.75,
|
||||
stroke,
|
||||
opacity: selected ? 1 : 0.85,
|
||||
}}
|
||||
/>
|
||||
{showLabel && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
}}
|
||||
className="pointer-events-none absolute"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEdges((eds) => eds.filter((e) => e.id !== id))}
|
||||
className={`pointer-events-auto cursor-pointer rounded-md border border-border/60 px-1.5 py-px font-mono text-[10px] font-semibold tabular-nums shadow-2xs backdrop-blur-xs transition-opacity hover:opacity-70 ${badge}`}
|
||||
title="点击隐藏该链路"
|
||||
>
|
||||
{formatNanoDelay(delay)}
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const LinkEdge = memo(LinkEdgeComponent)
|
||||
Reference in New Issue
Block a user