- 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
95 lines
2.5 KiB
TypeScript
95 lines
2.5 KiB
TypeScript
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)
|