import { useEffect, useState } from 'react'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { SystemSheet } from '@/components/ui/system-sheet'; import { Checkbox } from '@/components/ui/checkbox'; import type { MeshStackEntry } from '@/types/mesh'; import { Loader2 } from 'lucide-react'; interface Props { open: boolean; onOpenChange: (open: boolean) => void; nodeId: number; nodeName: string; onChanged: () => void; } export function MeshOptInSheet({ open, onOpenChange, nodeId, nodeName, onChanged }: Props) { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [pendingStack, setPendingStack] = useState(null); useEffect(() => { if (!open) return; let cancelled = false; (async () => { setLoading(true); setError(null); try { const res = await apiFetch(`/mesh/nodes/${nodeId}/stacks`, { localOnly: true }); if (!res.ok) throw new Error(`status ${res.status}`); const data = await res.json() as { stacks: MeshStackEntry[] }; if (!cancelled) setStacks(data.stacks); } catch (err) { if (!cancelled) setError((err as Error).message); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [open, nodeId]); const toggle = async (stack: MeshStackEntry) => { setPendingStack(stack.name); setError(null); try { const action = stack.optedIn ? 'opt-out' : 'opt-in'; const res = await apiFetch( `/mesh/nodes/${nodeId}/stacks/${encodeURIComponent(stack.name)}/${action}`, { method: 'POST', localOnly: true }, ); if (res.status === 409) { const body = await res.json().catch(() => ({})) as { error?: string }; setError(body.error || 'Port already claimed by another mesh stack'); return; } if (!res.ok) throw new Error(`status ${res.status}`); setStacks((prev) => prev.map((s) => s.name === stack.name ? { ...s, optedIn: !stack.optedIn } : s)); onChanged(); toast.success(stack.optedIn ? 'Stack removed from mesh' : 'Stack added to mesh'); } catch (err) { setError((err as Error).message); toast.error('Mesh update failed'); } finally { setPendingStack(null); } }; const inMeshCount = stacks.filter((s) => s.optedIn).length; const meta = `${inMeshCount} of ${stacks.length} in mesh`; return (

Adding a stack lets its services be reached from other meshed stacks by hostname. Toggling a stack redeploys it to refresh hostnames.

{loading && (
Loading stacks…
)} {error && (
{error}
)} {!loading && stacks.length === 0 && (
No stacks deployed on this node yet.
)}
{stacks.map((stack) => (
{ void toggle(stack); }} />
{pendingStack === stack.name && } {stack.optedIn && pendingStack !== stack.name && ( in mesh )}
))}
); }