import { Fragment, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Network as NetworkIcon, Plus, Trash2, Eraser, ChevronRight, ChevronDown, Link2, Unplug, } from "lucide-react"; import { toast } from "sonner"; import { Badge, Button, Card, Input, Spinner } from "@/components/ui"; import { ConfirmDialog } from "@/components/ui/ConfirmDialog"; import { HostHeader } from "@/components/hosts/HostHeader"; import { networksApi, type NetworkInfo } from "@/api/networks"; import { agentsApi } from "@/api/agents"; import { apiErrorMessage } from "@/api/client"; import { useAuthStore } from "@/store/auth"; import type { Agent } from "@/types"; const selectClass = "w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"; export function Networks() { const isAdmin = useAuthStore((s) => s.user?.role === "admin"); const agents = useQuery({ queryKey: ["agents"], queryFn: () => agentsApi.list(), refetchInterval: 15000, }); const hasAgents = (agents.data?.length ?? 0) > 0; return (
{agents.data?.map((agent) => ( ))}
); } function NetworksSection({ agent, isAdmin, showHostLabel, }: { agent?: Agent; isAdmin: boolean; showHostLabel: boolean; }) { const agentId = agent?.id; const online = !agent || agent.status === "online"; const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["networks", agentId ?? "local"], queryFn: () => networksApi.list(agentId), refetchInterval: 10000, enabled: online, }); const [creating, setCreating] = useState(false); const [toDelete, setToDelete] = useState(null); const [expanded, setExpanded] = useState(null); const invalidate = () => qc.invalidateQueries({ queryKey: ["networks", agentId ?? "local"] }); const colSpan = isAdmin ? 6 : 5; const prune = useMutation({ mutationFn: () => networksApi.prune(agentId), onSuccess: (r) => { const n = r.NetworksDeleted?.length ?? 0; toast.success(n ? `Pruned ${n} network(s)` : "No unused networks"); invalidate(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const remove = useMutation({ mutationFn: (id: string) => networksApi.remove(id, agentId), onSuccess: () => { toast.success("Network deleted"); setToDelete(null); invalidate(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const header = ( {isAdmin && online && ( <> )} ); return (
{(showHostLabel || (isAdmin && online)) && header} {!online ? (

Host is {agent?.status}. Check it under Settings → Remote hosts.

) : isLoading ? ( ) : ( {isAdmin && } {data?.map((n) => ( setExpanded((e) => (e === n.id ? null : n.id))} className="cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50" > {isAdmin && ( )} {expanded === n.id && ( )} ))} {data?.length === 0 && ( )}
Name Driver Scope Subnet In use
{expanded === n.id ? ( ) : ( )} {n.name} {n.is_default && default} {n.stack && {n.stack}} {n.internal && internal}
{n.driver} {n.scope} {n.subnet ?? "—"} {n.in_use ? ( {n.containers.length} container{n.containers.length > 1 ? "s" : ""} ) : ( )} {!n.is_default && ( )}
No networks.
)} {creating && ( { setCreating(false); invalidate(); }} onCancel={() => setCreating(false)} /> )} {toDelete && ( remove.mutate(toDelete.id)} onCancel={() => setToDelete(null)} /> )}
); } function NetworkDetail({ network, isAdmin, agentId, }: { network: NetworkInfo; isAdmin: boolean; agentId?: number; }) { const qc = useQueryClient(); const [pick, setPick] = useState(""); const { data, isLoading } = useQuery({ queryKey: ["network-containers", agentId ?? "local", network.id], queryFn: () => networksApi.containers(network.id, agentId), refetchInterval: 10000, }); const refresh = () => { qc.invalidateQueries({ queryKey: ["network-containers", agentId ?? "local", network.id] }); qc.invalidateQueries({ queryKey: ["networks", agentId ?? "local"] }); }; const connect = useMutation({ mutationFn: (container: string) => networksApi.connect(network.id, container, undefined, agentId), onSuccess: () => { toast.success("Container connected"); setPick(""); refresh(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const disconnect = useMutation({ mutationFn: (container: string) => networksApi.disconnect(network.id, container, false, agentId), onSuccess: () => { toast.success("Container disconnected"); refresh(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); const connected = (data ?? []).filter((c) => c.connected); const available = (data ?? []).filter((c) => !c.connected); return (

Connected containers

{isLoading ? ( ) : connected.length === 0 ? (

No containers connected.

) : (
    {connected.map((c) => (
  • {c.name} {c.stack && {c.stack}} {c.state} {isAdmin && !network.is_default && ( )}
  • ))}
)}
{isAdmin && (
)}
); } function Meta({ label, value, mono }: { label: string; value: string; mono?: boolean }) { return (
{label}

{value}

); } function CreateNetworkDialog({ agentId, onDone, onCancel, }: { agentId?: number; onDone: () => void; onCancel: () => void; }) { const [form, setForm] = useState({ name: "", driver: "bridge", subnet: "", gateway: "", internal: false, attachable: true, }); const set = (k: string, v: unknown) => setForm((f) => ({ ...f, [k]: v })); const create = useMutation({ mutationFn: () => networksApi.create( { name: form.name, driver: form.driver, subnet: form.subnet.trim() || null, gateway: form.gateway.trim() || null, internal: form.internal, attachable: form.attachable, }, agentId ), onSuccess: () => { toast.success("Network created"); onDone(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); return (
e.stopPropagation()} >

Create network

); }