Move Dockhand container management to its own top-level Docker page
Container management was buried inside the generic Integrations browsing view, mixed in with five unrelated integration types behind a single "pick any integration" dropdown. Give it a dedicated page, matching how Servers & Tasks and DNS already get their own top-level spot instead of living inside Integrations. New web/src/pages/Docker.tsx reuses the existing, unchanged Dockhand API routes and adapter (no server changes) — it just has its own integration picker scoped to Dockhand only, rather than sharing Integrations' any-type dropdown. Added a Docker nav item (right after Integrations) and route. Removed the Dockhand-specific state/handlers/table from Integrations entirely; adding, editing, enabling/disabling, and deleting the Dockhand credential itself still happens under Integrations -> Manage integrations like every other integration. Selecting a Dockhand row in Integrations' generic browsing dropdown now points to the Docker page instead of rendering a container table there too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e7ac6fea3b
commit
52dc7ed1f5
5 files changed
+233
-144
No files matched your search
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useState, type CSSProperties } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
api,
|
||||
type CurrentUser,
|
||||
type DockhandContainer,
|
||||
type DockhandContainersResponse,
|
||||
type GiteaRepo,
|
||||
type IntegrationSummary,
|
||||
type IntegrationType,
|
||||
@@ -81,22 +80,6 @@ function semaphoreStatusBadge(template: SemaphoreTemplate) {
|
||||
}
|
||||
}
|
||||
|
||||
function containerStateBadge(state: string) {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return <span className="badge bg-green-lt text-green">Running</span>;
|
||||
case "exited":
|
||||
case "dead":
|
||||
return <span className="badge bg-red-lt text-red">{state === "dead" ? "Dead" : "Exited"}</span>;
|
||||
case "paused":
|
||||
return <span className="badge bg-yellow-lt text-yellow">Paused</span>;
|
||||
case "restarting":
|
||||
return <span className="badge bg-blue-lt text-blue">Restarting</span>;
|
||||
default:
|
||||
return <span className="badge bg-secondary-lt text-secondary">{state}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
function runStatusBadge(repo: GiteaRepo) {
|
||||
const run = repo.latestRun;
|
||||
if (!run) return <span className="badge bg-secondary-lt text-secondary">No runs</span>;
|
||||
@@ -161,11 +144,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
const [loadingRepos, setLoadingRepos] = useState(false);
|
||||
const [rerunningRun, setRerunningRun] = useState<number | null>(null);
|
||||
|
||||
const [dockhandData, setDockhandData] = useState<DockhandContainersResponse | null>(null);
|
||||
const [dockhandError, setDockhandError] = useState<string | null>(null);
|
||||
const [loadingContainers, setLoadingContainers] = useState(false);
|
||||
const [actingOnContainer, setActingOnContainer] = useState<string | null>(null);
|
||||
|
||||
const [semaphoreData, setSemaphoreData] = useState<SemaphoreTemplatesResponse | null>(null);
|
||||
const [semaphoreError, setSemaphoreError] = useState<string | null>(null);
|
||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||
@@ -214,16 +192,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
.finally(() => setLoadingRepos(false));
|
||||
}
|
||||
|
||||
function loadDockhandContainers(id: number) {
|
||||
setLoadingContainers(true);
|
||||
setDockhandError(null);
|
||||
api.integrations.dockhand
|
||||
.containers(id)
|
||||
.then((res) => setDockhandData(res))
|
||||
.catch((err) => setDockhandError(err instanceof Error ? err.message : String(err)))
|
||||
.finally(() => setLoadingContainers(false));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selected?.type === "tailscale" && selected.enabled && !managing) {
|
||||
loadTailscaleDevices(selected.id);
|
||||
@@ -235,11 +203,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
} else {
|
||||
setGiteaRepos(null);
|
||||
}
|
||||
if (selected?.type === "dockhand" && selected.enabled && !managing) {
|
||||
loadDockhandContainers(selected.id);
|
||||
} else {
|
||||
setDockhandData(null);
|
||||
}
|
||||
if (selected?.type === "semaphore" && selected.enabled && !managing) {
|
||||
loadSemaphoreTemplates(selected.id);
|
||||
} else {
|
||||
@@ -343,21 +306,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function containerAction(c: DockhandContainer, action: "start" | "stop" | "restart") {
|
||||
if (!selectedId) return;
|
||||
if (action === "stop" && !confirm(`Stop container "${c.name}"?`)) return;
|
||||
setActingOnContainer(c.id);
|
||||
setDockhandError(null);
|
||||
try {
|
||||
await api.integrations.dockhand[action](selectedId, c.environmentId, c.id);
|
||||
loadDockhandContainers(selectedId);
|
||||
} catch (err) {
|
||||
setDockhandError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setActingOnContainer(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTemplate(t: SemaphoreTemplate) {
|
||||
if (!selectedId) return;
|
||||
if (!confirm(`Run "${t.name}" now?`)) return;
|
||||
@@ -650,94 +598,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : selected?.type === "dockhand" ? (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
Containers
|
||||
{dockhandData && (
|
||||
<span className="text-secondary fw-normal ms-2">
|
||||
{dockhandData.summary.running}/{dockhandData.summary.total} running
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => loadDockhandContainers(selected.id)}
|
||||
disabled={loadingContainers}
|
||||
>
|
||||
{loadingContainers ? "Refreshing…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{dockhandError && <div className="alert alert-danger m-3 mb-0">{dockhandError}</div>}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Container</th>
|
||||
<th>Host</th>
|
||||
<th>Image</th>
|
||||
<th>Status</th>
|
||||
{canEdit && <th className="w-1">Actions</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dockhandData?.containers.map((c) => (
|
||||
<tr key={`${c.environmentId}:${c.id}`}>
|
||||
<td>{c.name}</td>
|
||||
<td className="text-secondary">{c.environmentName}</td>
|
||||
<td className="text-secondary">{c.image}</td>
|
||||
<td>
|
||||
{containerStateBadge(c.state)}
|
||||
<div className="text-secondary small">{c.status}</div>
|
||||
</td>
|
||||
{canEdit && (
|
||||
<td>
|
||||
<div className="btn-list flex-nowrap">
|
||||
{c.state === "running" ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => containerAction(c, "restart")}
|
||||
disabled={actingOnContainer === c.id}
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
onClick={() => containerAction(c, "stop")}
|
||||
disabled={actingOnContainer === c.id}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => containerAction(c, "start")}
|
||||
disabled={actingOnContainer === c.id}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
{dockhandData?.containers.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={canEdit ? 5 : 4} className="text-secondary text-center">
|
||||
No containers found across any environment.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : selected?.type === "semaphore" ? (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
@@ -995,6 +855,12 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : selected?.type === "dockhand" ? (
|
||||
<div className="card">
|
||||
<div className="card-body text-secondary">
|
||||
Manage this integration's containers on the <Link to="/docker">Docker</Link> page.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card">
|
||||
<div className="card-body text-secondary">
|
||||
|
||||
Reference in new issue
Block a user