Files
KLALB-dashboard/src/components/topology/node-detail-sheet.tsx
T
SerinaNya 3f0b20e757 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
2026-08-24 23:59:00 +08:00

180 lines
5.9 KiB
TypeScript

import {
Server,
Copy,
Check,
Gauge,
Network,
Clock3,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { formatNanoDelay } from '@/lib/format'
export interface SheetNodeInfo {
id: string
address: string
compressedAddress?: string
isSelf: boolean
deviceName?: string
}
export interface SheetNeighborInfo {
address: string
deviceName?: string
delay: number
cost: number
}
interface NodeDetailSheetProps {
open: boolean
onOpenChange: (open: boolean) => void
node: SheetNodeInfo | null
neighbors: SheetNeighborInfo[]
copiedId: string | null
onCopy: (text: string) => void
}
export function NodeDetailSheet({
open,
onOpenChange,
node,
neighbors,
copiedId,
onCopy,
}: NodeDetailSheetProps) {
if (!node) return null
const isCopied = copiedId === node.address
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-full gap-0 sm:max-w-sm">
<SheetHeader className="border-b">
<div className="flex items-center gap-2.5 pt-4">
<div
className={`flex size-9 shrink-0 items-center justify-center rounded-lg ${
node.isSelf
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground'
}`}
>
<Server className="size-5" />
</div>
<div className="min-w-0">
<SheetTitle className="flex items-center gap-2 truncate">
{node.deviceName || '未命名节点'}
{node.isSelf && (
<Badge variant="default" className="px-1.5 py-0 text-[10px]">
本机
</Badge>
)}
</SheetTitle>
<SheetDescription className="truncate font-mono text-xs">
{node.compressedAddress || node.address}
</SheetDescription>
</div>
</div>
</SheetHeader>
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
{/* Full Address */}
<div className="flex flex-col gap-1.5 rounded-lg border bg-muted/30 p-3">
<span className="text-xs font-medium text-muted-foreground">
完整 SRv6 地址 (SID)
</span>
<div className="flex items-center justify-between gap-2">
<code className="break-all font-mono text-xs font-semibold text-foreground select-all">
{node.address}
</code>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="shrink-0"
onClick={() => onCopy(node.address)}
title="复制完整地址"
>
{isCopied ? (
<Check className="size-3.5 text-emerald-500" />
) : (
<Copy className="size-3.5" />
)}
</Button>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col gap-0.5 rounded-lg border bg-card p-2.5">
<div className="flex items-center gap-1 text-[11px] text-muted-foreground">
<Network className="size-3" />
邻居数量
</div>
<span className="font-mono text-lg font-bold">{neighbors.length}</span>
</div>
<div className="flex flex-col gap-0.5 rounded-lg border bg-card p-2.5">
<div className="flex items-center gap-1 text-[11px] text-muted-foreground">
<Gauge className="size-3" />
平均时延
</div>
<span className="font-mono text-lg font-bold">
{neighbors.length > 0
? formatNanoDelay(
neighbors.reduce((acc, n) => acc + n.delay, 0) / neighbors.length
)
: '--'}
</span>
</div>
</div>
<Separator />
{/* Neighbor list */}
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-muted-foreground">邻居链路</span>
{neighbors.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
暂无可达邻居节点
</div>
) : (
neighbors.map((nb) => (
<div
key={nb.address}
className="flex items-center justify-between gap-2 rounded-lg border bg-card px-2.5 py-2"
>
<div className="flex min-w-0 flex-col leading-tight">
<span className="truncate text-xs font-medium text-foreground">
{nb.deviceName || '未命名节点'}
</span>
<span className="truncate font-mono text-[10px] text-muted-foreground">
{nb.address}
</span>
</div>
<div className="flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-[11px] font-semibold">
<Clock3 className="size-3" />
{formatNanoDelay(nb.delay)}
</div>
</div>
))
)}
</div>
{/* Reserved for Kperf speed test */}
<Button variant="outline" size="sm" disabled title="测速功能即将接入">
<Gauge data-icon="inline-start" />
发起 Kperf 测速(即将支持)
</Button>
</div>
</SheetContent>
</Sheet>
)
}