import { memo, useMemo, type ReactElement } from "react"; import type { NodeInfo } from "../api"; export interface MeshTopologyProps { nodes: NodeInfo[]; className?: string; } const STATUS_COLORS: Record = { online: "var(--success, var(--color-success))", offline: "var(--text-dim)", connecting: "var(--triage)", error: "var(--color-error)", }; const NODE_RADIUS = 28; const LABEL_OFFSET = 12; const MIN_VIEWBOX_SIZE = 300; const MAX_REMOTE_DISTANCE = 120; function MeshTopologyInner({ nodes, className }: MeshTopologyProps): ReactElement { // Find the local node (center) const localNode = useMemo(() => { return nodes.find((n) => n.type === "local") ?? nodes[0]; }, [nodes]); // Remote nodes arranged in a circle around the local node const remoteNodes = useMemo(() => { return nodes.filter((n) => n.type === "remote"); }, [nodes]); // Calculate SVG dimensions based on number of remote nodes const viewBoxSize = useMemo(() => { const baseSize = MIN_VIEWBOX_SIZE; const extraForRemotes = Math.max(0, remoteNodes.length - 4) * 20; return baseSize + extraForRemotes; }, [remoteNodes.length]); const centerX = viewBoxSize / 2; const centerY = viewBoxSize / 2; // Calculate positions for remote nodes in a circle const remotePositions = useMemo(() => { if (remoteNodes.length === 0) return []; const distance = Math.min(MAX_REMOTE_DISTANCE, (viewBoxSize / 2) - NODE_RADIUS - 10); const angleStep = (2 * Math.PI) / remoteNodes.length; const startAngle = -Math.PI / 2; // Start from top return remoteNodes.map((node, index) => { const angle = startAngle + index * angleStep; return { node, x: centerX + distance * Math.cos(angle), y: centerY + distance * Math.sin(angle), }; }); }, [remoteNodes, viewBoxSize, centerX, centerY]); // Build peer awareness lines (lines between remote nodes that share knowledge) const peerLines = useMemo(() => { // Simple heuristic: show lines between remote nodes that share knowledge const lines: Array<{ x1: number; y1: number; x2: number; y2: number }> = []; // Only draw peer lines if we have more than 2 remote nodes if (remotePositions.length > 2) { remotePositions.forEach((rp, index) => { // Draw a subtle line to one other node for visual interest if (index % 2 === 0 && index + 1 < remotePositions.length) { const other = remotePositions[index + 1]; lines.push({ x1: rp.x, y1: rp.y, x2: other.x, y2: other.y }); } }); } return lines; }, [remotePositions]); if (nodes.length === 0) { return (

No nodes to display

); } return (
{/* Peer awareness lines between remote nodes */} {peerLines.map((line, index) => ( ))} {/* Lines from local node to remote nodes */} {remotePositions.map((rp) => ( ))} {/* Local node (center) */} {localNode && ( {localNode.name.length > 12 ? `${localNode.name.slice(0, 10)}…` : localNode.name} {localNode.type === "local" ? "🏠" : "🌐"} )} {/* Remote nodes */} {remotePositions.map((rp) => ( {rp.node.name.length > 12 ? `${rp.node.name.slice(0, 10)}…` : rp.node.name} 🌐 ))} {/* Legend */}
Online
Offline
Connecting
Error
); } export const MeshTopology = memo(MeshTopologyInner);