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]); if (nodes.length === 0) { return (

No nodes to display

); } return (
{/* 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" ? "L" : "R"} )} {/* Remote nodes */} {remotePositions.map((rp) => ( {rp.node.name.length > 12 ? `${rp.node.name.slice(0, 10)}…` : rp.node.name} {rp.node.type === "local" ? "L" : "R"} ))} {/* Legend */}
Online
Offline
Connecting
Error

Peer-to-peer discovery data unavailable.

); } export const MeshTopology = memo(MeshTopologyInner);