import { useCallback, useEffect, useState } from 'react'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { Button } from '@/components/ui/button'; import { ArrowLeftRight, Loader2, ScrollText } from 'lucide-react'; import { RoutingNodeCard } from './RoutingNodeCard'; import { MeshOptInSheet } from './MeshOptInSheet'; import { MeshRouteDetailSheet } from './MeshRouteDetailSheet'; import { MeshDiagnosticsSheet } from './MeshDiagnosticsSheet'; import { MeshActivitySheet } from './MeshActivitySheet'; import type { MeshAlias, MeshNodeStatus, MeshProbeResult } from '@/types/mesh'; export function RoutingTab() { const [status, setStatus] = useState([]); const [aliases, setAliases] = useState([]); const [loading, setLoading] = useState(true); const [optInNode, setOptInNode] = useState<{ id: number; name: string } | null>(null); const [diagnosticsNode, setDiagnosticsNode] = useState<{ id: number; name: string } | null>(null); const [routeDetailAlias, setRouteDetailAlias] = useState(null); const [activityOpen, setActivityOpen] = useState(false); const refresh = useCallback(async () => { try { const [statusRes, aliasesRes] = await Promise.all([ apiFetch('/mesh/status', { localOnly: true }), apiFetch('/mesh/aliases', { localOnly: true }), ]); if (statusRes.ok) { const body = await statusRes.json() as { nodes: MeshNodeStatus[] }; setStatus(body.nodes); } if (aliasesRes.ok) { const body = await aliasesRes.json() as { aliases: MeshAlias[] }; setAliases(body.aliases); } } catch (err) { toast.error(`Failed to load mesh state: ${(err as Error).message}`); } finally { setLoading(false); } }, []); useEffect(() => { void refresh(); }, [refresh]); const testUpstream = useCallback(async (alias: string): Promise => { try { const res = await apiFetch(`/mesh/aliases/${encodeURIComponent(alias)}/test`, { method: 'POST', localOnly: true, }); const body = await res.json() as MeshProbeResult; if (body.ok) { toast.success(`${alias} ok (${body.latencyMs}ms)`); } else { toast.error(`${alias} ${body.where ?? 'fail'}: ${body.code ?? 'error'}`); } } catch (err) { toast.error(`Probe failed: ${(err as Error).message}`); } }, []); const totalAliases = aliases.length; const meshedNodes = status.filter((s) => s.enabled).length; const onlineNodes = status.filter((s) => s.pilotConnected).length; if (loading) { return (
Loading mesh state…
); } if (status.length === 0) { return (
No nodes available
Add a node to the fleet to start routing traffic between containers across nodes.
); } if (meshedNodes === 0) { return (
setActivityOpen(true)} />
Mesh containers across nodes
Add a stack to the mesh and its services become reachable from any other meshed stack by hostname. No VPN, no firewall changes.
{status.map((s) => ( setOptInNode({ id: s.nodeId, name: s.nodeName })} onShowDiagnostics={() => setDiagnosticsNode({ id: s.nodeId, name: s.nodeName })} onShowAlias={(alias) => setRouteDetailAlias(alias)} onTestUpstream={testUpstream} onChanged={() => { void refresh(); }} /> ))}
{ void refresh(); }} />
); } return (
setActivityOpen(true)} />
{status.map((s) => ( setOptInNode({ id: s.nodeId, name: s.nodeName })} onShowDiagnostics={() => setDiagnosticsNode({ id: s.nodeId, name: s.nodeName })} onShowAlias={(alias) => setRouteDetailAlias(alias)} onTestUpstream={testUpstream} onChanged={() => { void refresh(); }} /> ))}
{ void refresh(); }} />
); } function RoutingMasthead({ meshedNodes, onlineNodes, totalAliases, onShowActivity }: { meshedNodes: number; onlineNodes: number; totalAliases: number; onShowActivity: () => void; }) { const stateWord = meshedNodes === 0 ? 'unmeshed' : meshedNodes < onlineNodes ? 'partial' : 'meshed'; return (
{stateWord}
meshed
{meshedNodes}/{onlineNodes}
aliases
{totalAliases}
); } function SheetsRoot(props: { optInNode: { id: number; name: string } | null; setOptInNode: (v: { id: number; name: string } | null) => void; diagnosticsNode: { id: number; name: string } | null; setDiagnosticsNode: (v: { id: number; name: string } | null) => void; routeDetailAlias: string | null; setRouteDetailAlias: (v: string | null) => void; activityOpen: boolean; setActivityOpen: (v: boolean) => void; onChanged: () => void; }) { return ( <> {props.optInNode && ( { if (!open) props.setOptInNode(null); }} nodeId={props.optInNode.id} nodeName={props.optInNode.name} onChanged={props.onChanged} /> )} { if (!open) props.setDiagnosticsNode(null); }} nodeId={props.diagnosticsNode?.id ?? null} nodeName={props.diagnosticsNode?.name ?? null} /> { if (!open) props.setRouteDetailAlias(null); }} alias={props.routeDetailAlias} /> ); }