import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { Plus, Pencil, Trash2, RefreshCw, AlertCircle, Store } from 'lucide-react'; import { connectivityAPI } from '../../services/api'; import { useConnectivityData } from './useConnectivityData'; import { Toast, useToasts, toastEvent, apiError, typeMeta, GROUP_TYPES, GROUP_LABELS, LifecycleBadge, HealthDot, TypeIcon, lifecycleOf, buildRefCounts, } from './shared'; function DeleteDialog({ conn, onConfirm, onCancel, deleting }) { return (

Delete "{conn.name}"?

This removes the connection. Peers or services assigned to it must be re-pointed first โ€” the API blocks deletion while it is in use.

); } export default function ConnectionListPage({ group }) { const toasts = useToasts(); const navigate = useNavigate(); const { connections, peerExits, serviceEgress, installed, loading, error, reload, } = useConnectivityData(); const [delTarget, setDelTarget] = useState(null); const [deleting, setDeleting] = useState(false); const types = GROUP_TYPES[group] || []; const rows = connections.filter((c) => types.includes(c.type)); const refs = buildRefCounts(peerExits, serviceEgress); // The backing store service for this group is considered installed if any of // its types' service ids is present in the installed map. const anyServiceInstalled = types.some((t) => { const svc = typeMeta(t).service; return svc && installed[svc]; }); const handleDelete = async () => { setDeleting(true); try { await connectivityAPI.deleteConnection(delTarget.id); toastEvent(`Deleted ${delTarget.name}`); setDelTarget(null); await reload(); } catch (err) { // 409 โ†’ in use; surface the API's specific message. toastEvent(apiError(err, `Failed to delete ${delTarget.name}`), 'error'); } finally { setDeleting(false); } }; const title = GROUP_LABELS[group] || group; // For single-type groups the "Add" button targets that type; multi-type // groups default to the first type (the form lets the user switch). const addType = types[0]; return (

{title}

{group === 'tor' ? 'A single Tor exit for anonymised peer traffic' : `Manage ${title.toLowerCase()} exit connections`}

{/* Tor is a singleton โ€” hide Add once one exists */} {!(group === 'tor' && rows.length > 0) && ( Add {typeMeta(addType).short} )}
{loading && (
)} {!loading && error && (

Failed to load connections

{error}

)} {!loading && !error && rows.length === 0 && (

No {title.toLowerCase()} configured yet

{anyServiceInstalled ? `Add your first ${typeMeta(addType).short} connection to route peer traffic through it.` : 'The backing service is not installed yet.'}

Add {typeMeta(addType).short} {!anyServiceInstalled && ( Open Store )}
)} {!loading && !error && rows.length > 0 && (
{rows.map((conn) => { const meta = typeMeta(conn.type); const ref = refs[conn.id] || { peers: 0, services: 0 }; const lifecycle = lifecycleOf(conn, ref.peers + ref.services); return ( ); })}
Name Type Lifecycle Health Used by
{conn.name}
{meta.label}
{ref.peers} peer{ref.peers === 1 ? '' : 's'} ยท {ref.services} svc
)} {delTarget && ( setDelTarget(null)} /> )}
); }