import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api, type CurrentUser, type DockhandContainer, type DockhandContainersResponse, type IntegrationSummary, } from "../api/client"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { downloadCsv } from "../utils/csv"; function updateBadge(c: DockhandContainer) { if (c.updateAvailable === null) return —; if (!c.updateAvailable) return Up to date; return ( Update available ); } function containerStateBadge(state: string) { switch (state) { case "running": return Running; case "exited": case "dead": return {state === "dead" ? "Dead" : "Exited"}; case "paused": return Paused; case "restarting": return Restarting; default: return {state}; } } export default function Docker({ user }: { user: CurrentUser }) { const canEdit = user.role === "admin" || user.role === "operator"; const [integrations, setIntegrations] = useState(null); const [selectedId, setSelectedId] = useState(null); const [error, setError] = useState(null); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [actingOnContainer, setActingOnContainer] = useState(null); const [hostFilter, setHostFilter] = useState(""); const [checkingUpdates, setCheckingUpdates] = useState(false); useEffect(() => { api.integrations .list() .then((res) => { const dockhandIntegrations = res.integrations.filter((i) => i.type === "dockhand"); setIntegrations(dockhandIntegrations); if (!selectedId && dockhandIntegrations.length > 0) setSelectedId(dockhandIntegrations[0].id); }) .catch((err) => setError(err instanceof Error ? err.message : String(err))); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const selected = integrations?.find((i) => i.id === selectedId) ?? null; function loadContainers(id: number) { setLoading(true); setError(null); api.integrations.dockhand .containers(id) .then((res) => setData(res)) .catch((err) => setError(err instanceof Error ? err.message : String(err))) .finally(() => setLoading(false)); } useEffect(() => { setHostFilter(""); if (selected?.enabled) { loadContainers(selected.id); } else { setData(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedId]); const hosts = [...new Set((data?.containers ?? []).map((c) => c.environmentName))].sort(); const filteredContainers = (data?.containers ?? []).filter((c) => !hostFilter || c.environmentName === hostFilter); const { sorted, sortKey, sortDir, requestSort } = useSortable(filteredContainers); function exportCsv() { if (!sorted) return; downloadCsv( "docker-containers.csv", ["Container", "Host", "Image", "State", "Status", "Update available"], sorted.map((c) => [ c.name, c.environmentName, c.image, c.state, c.status, c.updateAvailable === null ? "unknown" : c.updateAvailable ? "yes" : "no", ]), ); } async function checkForUpdates() { if (!selectedId) return; setCheckingUpdates(true); setError(null); try { await api.integrations.dockhand.checkForUpdates(selectedId); loadContainers(selectedId); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setCheckingUpdates(false); } } async function containerAction(c: DockhandContainer, action: "start" | "stop" | "restart") { if (!selectedId) return; if (action === "stop" && !confirm(`Stop container "${c.name}"?`)) return; setActingOnContainer(c.id); setError(null); try { await api.integrations.dockhand[action](selectedId, c.environmentId, c.id); loadContainers(selectedId); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setActingOnContainer(null); } } return ( <>

Docker

{error &&
{error}
} {integrations?.length === 0 ? (
No Dockhand integration configured yet.{" "} Add one under Integrations.
) : ( <> {integrations && integrations.length > 1 && (
)} {selected && !selected.enabled ? (
"{selected.name}" is disabled. Enable it under Integrations → Manage integrations to see its containers.
) : (

Containers {data && ( {data.summary.running}/{data.summary.total} running )} {data && data.containers.filter((c) => c.updateAvailable).length > 0 && ( {data.containers.filter((c) => c.updateAvailable).length} update(s) available )}

{hosts.length > 1 && ( )} {canEdit && ( )}
label="Container" sortKeyName="name" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Host" sortKeyName="environmentName" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Image" sortKeyName="image" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Status" sortKeyName="state" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Update" sortKeyName="updateAvailable" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> {canEdit && } {sorted?.map((c) => ( {canEdit && ( )} ))} {data && sorted?.length === 0 && ( )}
Actions
{c.name} {c.environmentName} {c.image} {containerStateBadge(c.state)}
{c.status}
{updateBadge(c)}
{c.state === "running" ? ( <> ) : ( )}
{hostFilter ? `No containers found on "${hostFilter}".` : "No containers found across any environment."}
)} )} ); }