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 { networksApi, type NetworkInfo } from "@/api/networks"; import { apiErrorMessage } from "@/api/client"; import { useAuthStore } from "@/store/auth"; 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"); return ; } function NetworksSection({ isAdmin }: { isAdmin: boolean }) { const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ["networks"], queryFn: () => networksApi.list(), refetchInterval: 60000, }); const [creating, setCreating] = useState(false); const [toDelete, setToDelete] = useState(null); const [expanded, setExpanded] = useState(null); const invalidate = () => qc.invalidateQueries({ queryKey: ["networks"] }); const colSpan = isAdmin ? 6 : 5; const prune = useMutation({ mutationFn: () => networksApi.prune(), 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), onSuccess: () => { toast.success("Network deleted"); setToDelete(null); invalidate(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); return (
{isAdmin && (
)} {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, }: { network: NetworkInfo; isAdmin: boolean; }) { const qc = useQueryClient(); const [pick, setPick] = useState(""); const { data, isLoading } = useQuery({ queryKey: ["network-containers", network.id], queryFn: () => networksApi.containers(network.id), refetchInterval: 60000, }); const refresh = () => { qc.invalidateQueries({ queryKey: ["network-containers", network.id] }); qc.invalidateQueries({ queryKey: ["networks"] }); }; const connect = useMutation({ mutationFn: (container: string) => networksApi.connect(network.id, container), 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), 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({ onDone, onCancel, }: { 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, }), onSuccess: () => { toast.success("Network created"); onDone(); }, onError: (e) => toast.error(apiErrorMessage(e)), }); return (
e.stopPropagation()} >

Create network

); }