import { useCallback, useEffect, useMemo, useRef } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
Handle,
Position,
type Node,
type Edge,
type NodeTypes,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Server, Radio, RadioTower, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
buildTunnelsGraph,
buildAliasesGraph,
type MeshNodeData,
} from '@/lib/mesh-topology-layout';
import type { MeshAlias, MeshNodeStatus, MeshReachableMode } from '@/types/mesh';
const MINIMAP_BRAND = 'oklch(0.78 0.11 195)';
const MINIMAP_WARNING = 'oklch(0.75 0.14 75)';
const MINIMAP_MUTED = 'oklch(0.55 0 0)';
const MINIMAP_DESTRUCTIVE = 'oklch(0.65 0.2 28)';
export type MeshGraphEdgeMode = 'tunnels' | 'aliases';
interface MeshTopologyGraphProps {
status: MeshNodeStatus[];
aliases: MeshAlias[];
edgeMode: MeshGraphEdgeMode;
onNodeClick?: (nodeId: number) => void;
}
function statusDot(node: MeshNodeStatus): string {
if (node.reachableMode === 'unreachable') return 'bg-destructive';
if (node.reachableMode === 'pilot' && !node.pilotConnected) return 'bg-warning';
if (!node.enabled) return 'bg-muted-foreground';
return 'bg-success';
}
function reachableModeLabel(mode: MeshReachableMode): string {
switch (mode) {
case 'local': return 'local';
case 'pilot': return 'pilot';
case 'proxy': return 'proxy';
case 'unreachable': return 'unreachable';
}
}
function ModeIcon({ mode, connected }: { mode: MeshReachableMode; connected: boolean }) {
if (mode === 'unreachable') return ;
if (mode === 'pilot' && !connected) return ;
if (mode === 'pilot') return ;
return null;
}
function MeshNodeCard({ data, selected }: { data: MeshNodeData; selected?: boolean }) {
const node = data.node;
const isLocal = node.reachableMode === 'local';
const dimmed = node.reachableMode === 'unreachable';
const optedInCount = node.optedInStacks.length;
const stackLabel = optedInCount === 1 ? 'stack' : 'stacks';
const streamLabel = node.activeStreamCount === 1 ? 'stream' : 'streams';
return (
{node.enabled ? (dimmed ? 'unreachable' : 'meshed') : 'unmeshed'}
{isLocal ? 'Local' : 'Remote'}
{node.nodeName}
mode
{reachableModeLabel(node.reachableMode)}
opted in
{optedInCount} {stackLabel}
{data.aliasCount > 0 && (
publishes
{data.aliasCount}
)}
{node.activeStreamCount} {streamLabel}
{node.reachableMode === 'unreachable' && node.reachableReason ? (
ยท {node.reachableReason}
) : null}
);
}
const nodeTypes: NodeTypes = {
meshNode: MeshNodeCard,
};
function nodeStateEqual(a: MeshNodeStatus, b: MeshNodeStatus): boolean {
return a.nodeId === b.nodeId
&& a.enabled === b.enabled
&& a.reachableMode === b.reachableMode
&& a.pilotConnected === b.pilotConnected
&& a.reachableReason === b.reachableReason
&& a.activeStreamCount === b.activeStreamCount
&& a.optedInStacks.length === b.optedInStacks.length;
}
export function MeshTopologyGraph({ status, aliases, edgeMode, onNodeClick }: MeshTopologyGraphProps) {
// Mirror FleetTopology's onNodeClick ref pattern: handlers may change on every
// parent render, but ReactFlow's onNodeClick is stable, so route through a ref
// to avoid recreating the callback (and thus rebuilding the flow) on every render.
const onNodeClickRef = useRef(onNodeClick);
onNodeClickRef.current = onNodeClick;
// Shape key: relayout only when nodes are added/removed or reachability flips.
const shapeKey = useMemo(
() => status
.map((n) => `${n.nodeId}:${n.reachableMode}:${n.enabled ? 1 : 0}:${n.pilotConnected ? 1 : 0}`)
.sort()
.join('|'),
[status],
);
const [flowNodes, setFlowNodes, onNodesChange] = useNodesState([]);
const [flowEdges, setFlowEdges, onEdgesChange] = useEdgesState([]);
useEffect(() => {
const result = edgeMode === 'tunnels'
? buildTunnelsGraph(status)
: buildAliasesGraph(status, aliases);
setFlowNodes(result.nodes);
setFlowEdges(result.edges);
// Intentionally exclude `status` and `aliases` raw refs so we only relayout when
// the shape (or selected edgeMode) changes; per-poll metric tweaks fall through
// the live-update effect below without resetting user-dragged positions.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shapeKey, edgeMode, setFlowNodes, setFlowEdges]);
// Refresh node card data on prop change without touching positions. The /mesh/status
// poll yields fresh JSON objects every cycle, so a reference equality check would
// replace data on every poll and defeat the purpose. Compare by value instead.
useEffect(() => {
setFlowNodes((current) => current.map((flowNode) => {
const next = status.find((n) => String(n.nodeId) === flowNode.id);
if (!next) return flowNode;
const existing = (flowNode.data as MeshNodeData | undefined);
const aliasCount = edgeMode === 'aliases'
? aliases.filter((a) => a.nodeId === next.nodeId).length
: 0;
if (
existing
&& existing.aliasCount === aliasCount
&& nodeStateEqual(existing.node, next)
) {
return flowNode;
}
return { ...flowNode, data: { node: next, aliasCount, isOwnerView: false } satisfies MeshNodeData };
}));
}, [status, aliases, edgeMode, setFlowNodes]);
const handleNodeClick = useCallback((_event: React.MouseEvent, flowNode: Node) => {
const id = Number(flowNode.id);
if (!Number.isNaN(id)) {
onNodeClickRef.current?.(id);
}
}, []);
const miniMapNodeColor = useCallback((n: Node) => {
const data = n.data as MeshNodeData | undefined;
const topo = data?.node;
if (!topo) return MINIMAP_MUTED;
if (topo.reachableMode === 'unreachable') return MINIMAP_DESTRUCTIVE;
if (topo.reachableMode === 'pilot' && !topo.pilotConnected) return MINIMAP_WARNING;
if (!topo.enabled) return MINIMAP_MUTED;
return MINIMAP_BRAND;
}, []);
return (
);
}