Add a host filter to the Docker page

Dockhand aggregates containers across every Docker host it manages
into one list, with no way to narrow it down to a single host. Add a
host dropdown next to Refresh (only shown when more than one host is
present) that filters the table client-side over the already-fetched
container list. Resets whenever the selected Dockhand integration
changes, so a stale host name from a previous integration can't linger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-16 00:01:17 +02:00
1 parent 52dc7ed1f5
commit 35afcc4a37
1 file changed
+24 -4
+24 -4
View File
@@ -34,6 +34,7 @@ export default function Docker({ user }: { user: CurrentUser }) {
const [data, setData] = useState<DockhandContainersResponse | null>(null); const [data, setData] = useState<DockhandContainersResponse | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [actingOnContainer, setActingOnContainer] = useState<string | null>(null); const [actingOnContainer, setActingOnContainer] = useState<string | null>(null);
const [hostFilter, setHostFilter] = useState("");
useEffect(() => { useEffect(() => {
api.integrations api.integrations
@@ -60,6 +61,7 @@ export default function Docker({ user }: { user: CurrentUser }) {
} }
useEffect(() => { useEffect(() => {
setHostFilter("");
if (selected?.enabled) { if (selected?.enabled) {
loadContainers(selected.id); loadContainers(selected.id);
} else { } else {
@@ -68,6 +70,9 @@ export default function Docker({ user }: { user: CurrentUser }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId]); }, [selectedId]);
const hosts = [...new Set((data?.containers ?? []).map((c) => c.environmentName))].sort();
const filteredContainers = (data?.containers ?? []).filter((c) => !hostFilter || c.environmentName === hostFilter);
async function containerAction(c: DockhandContainer, action: "start" | "stop" | "restart") { async function containerAction(c: DockhandContainer, action: "start" | "stop" | "restart") {
if (!selectedId) return; if (!selectedId) return;
if (action === "stop" && !confirm(`Stop container "${c.name}"?`)) return; if (action === "stop" && !confirm(`Stop container "${c.name}"?`)) return;
@@ -132,7 +137,22 @@ export default function Docker({ user }: { user: CurrentUser }) {
</span> </span>
)} )}
</h3> </h3>
<div className="card-actions"> <div className="card-actions d-flex gap-2">
{hosts.length > 1 && (
<select
className="form-select form-select-sm"
style={{ width: "auto" }}
value={hostFilter}
onChange={(e) => setHostFilter(e.target.value)}
>
<option value="">All hosts</option>
{hosts.map((host) => (
<option key={host} value={host}>
{host}
</option>
))}
</select>
)}
<button <button
className="btn btn-sm btn-outline-secondary" className="btn btn-sm btn-outline-secondary"
onClick={() => selected && loadContainers(selected.id)} onClick={() => selected && loadContainers(selected.id)}
@@ -154,7 +174,7 @@ export default function Docker({ user }: { user: CurrentUser }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.containers.map((c) => ( {filteredContainers.map((c) => (
<tr key={`${c.environmentId}:${c.id}`}> <tr key={`${c.environmentId}:${c.id}`}>
<td>{c.name}</td> <td>{c.name}</td>
<td className="text-secondary">{c.environmentName}</td> <td className="text-secondary">{c.environmentName}</td>
@@ -197,10 +217,10 @@ export default function Docker({ user }: { user: CurrentUser }) {
)} )}
</tr> </tr>
))} ))}
{data?.containers.length === 0 && ( {data && filteredContainers.length === 0 && (
<tr> <tr>
<td colSpan={canEdit ? 5 : 4} className="text-secondary text-center"> <td colSpan={canEdit ? 5 : 4} className="text-secondary text-center">
No containers found across any environment. {hostFilter ? `No containers found on "${hostFilter}".` : "No containers found across any environment."}
</td> </td>
</tr> </tr>
)} )}