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
@@ -39,9 +39,12 @@ All modules from the original plan are built:
|
|||||||
- **Integrations → Gitea** — repo list with each repo's last CI run status,
|
- **Integrations → Gitea** — repo list with each repo's last CI run status,
|
||||||
and re-running just the failed jobs in a run; a live repo-count widget
|
and re-running just the failed jobs in a run; a live repo-count widget
|
||||||
(with a failing-build warning).
|
(with a failing-build warning).
|
||||||
- **Integrations → Dockhand** — container status across every Docker host
|
- **Docker** (its own top-level page, backed by the Dockhand
|
||||||
Dockhand manages (one credential covers all of them), with
|
integration) — container status across every Docker host Dockhand
|
||||||
start/stop/restart actions; a live running/total widget.
|
manages (one credential covers all of them), with start/stop/restart
|
||||||
|
actions and a live running/total widget. Add/edit/remove the
|
||||||
|
Dockhand credential itself under Integrations → Manage integrations
|
||||||
|
as with every other integration.
|
||||||
- **Integrations → Semaphore** — Ansible run status per template across
|
- **Integrations → Semaphore** — Ansible run status per template across
|
||||||
every project, with a "Run" action to trigger a template; a live
|
every project, with a "Run" action to trigger a template; a live
|
||||||
template-count widget (with a last-failed warning).
|
template-count widget (with a last-failed warning).
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Dns from "./pages/Dns";
|
|||||||
import ServersTasks from "./pages/ServersTasks";
|
import ServersTasks from "./pages/ServersTasks";
|
||||||
import ServerDetail from "./pages/ServerDetail";
|
import ServerDetail from "./pages/ServerDetail";
|
||||||
import Integrations from "./pages/Integrations";
|
import Integrations from "./pages/Integrations";
|
||||||
|
import Docker from "./pages/Docker";
|
||||||
import Settings from "./pages/Settings";
|
import Settings from "./pages/Settings";
|
||||||
import NotificationSettings from "./pages/settings/NotificationSettings";
|
import NotificationSettings from "./pages/settings/NotificationSettings";
|
||||||
import BadgeSettings from "./pages/settings/BadgeSettings";
|
import BadgeSettings from "./pages/settings/BadgeSettings";
|
||||||
@@ -72,6 +73,7 @@ export default function App() {
|
|||||||
<Route path="/ipam" element={<Ipam user={user} />} />
|
<Route path="/ipam" element={<Ipam user={user} />} />
|
||||||
<Route path="/secrets" element={<Secrets user={user} />} />
|
<Route path="/secrets" element={<Secrets user={user} />} />
|
||||||
<Route path="/integrations" element={<Integrations user={user} />} />
|
<Route path="/integrations" element={<Integrations user={user} />} />
|
||||||
|
<Route path="/docker" element={<Docker user={user} />} />
|
||||||
<Route
|
<Route
|
||||||
path="/users"
|
path="/users"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
IconNetwork,
|
IconNetwork,
|
||||||
IconKey,
|
IconKey,
|
||||||
IconPlugConnected,
|
IconPlugConnected,
|
||||||
|
IconBrandDocker,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
@@ -28,6 +29,7 @@ const NAV_ITEMS: NavItem[] = [
|
|||||||
{ to: "/ipam", label: "IP Addresses", icon: <IconNetwork size={20} /> },
|
{ to: "/ipam", label: "IP Addresses", icon: <IconNetwork size={20} /> },
|
||||||
{ to: "/secrets", label: "Secrets", icon: <IconKey size={20} /> },
|
{ to: "/secrets", label: "Secrets", icon: <IconKey size={20} /> },
|
||||||
{ to: "/integrations", label: "Integrations", icon: <IconPlugConnected size={20} /> },
|
{ to: "/integrations", label: "Integrations", icon: <IconPlugConnected size={20} /> },
|
||||||
|
{ to: "/docker", label: "Docker", icon: <IconBrandDocker size={20} /> },
|
||||||
{ to: "/users", label: "Users", icon: <IconUsers size={20} />, minRole: "admin" },
|
{ to: "/users", label: "Users", icon: <IconUsers size={20} />, minRole: "admin" },
|
||||||
{ to: "/audit-log", label: "Audit Log", icon: <IconHistory size={20} />, minRole: "operator" },
|
{ to: "/audit-log", label: "Audit Log", icon: <IconHistory size={20} />, minRole: "operator" },
|
||||||
{ to: "/settings", label: "Settings", icon: <IconSettings size={20} />, minRole: "admin" },
|
{ to: "/settings", label: "Settings", icon: <IconSettings size={20} />, minRole: "admin" },
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
api,
|
||||||
|
type CurrentUser,
|
||||||
|
type DockhandContainer,
|
||||||
|
type DockhandContainersResponse,
|
||||||
|
type IntegrationSummary,
|
||||||
|
} from "../api/client";
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Docker({ user }: { user: CurrentUser }) {
|
||||||
|
const canEdit = user.role === "admin" || user.role === "operator";
|
||||||
|
|
||||||
|
const [integrations, setIntegrations] = useState<IntegrationSummary[] | null>(null);
|
||||||
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [data, setData] = useState<DockhandContainersResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [actingOnContainer, setActingOnContainer] = useState<string | null>(null);
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
if (selected?.enabled) {
|
||||||
|
loadContainers(selected.id);
|
||||||
|
} else {
|
||||||
|
setData(null);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedId]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<h2 className="page-title mb-3">Docker</h2>
|
||||||
|
{error && <div className="alert alert-danger">{error}</div>}
|
||||||
|
|
||||||
|
{integrations?.length === 0 ? (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body text-secondary">
|
||||||
|
No Dockhand integration configured yet.{" "}
|
||||||
|
<Link to="/integrations">Add one under Integrations</Link>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{integrations && integrations.length > 1 && (
|
||||||
|
<div className="mb-3" style={{ maxWidth: 320 }}>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={selectedId ?? ""}
|
||||||
|
onChange={(e) => setSelectedId(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{integrations.map((i) => (
|
||||||
|
<option key={i.id} value={i.id} disabled={!i.enabled}>
|
||||||
|
{i.name}
|
||||||
|
{!i.enabled ? " — disabled" : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selected && !selected.enabled ? (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body text-secondary">
|
||||||
|
"{selected.name}" is disabled. Enable it under Integrations → Manage integrations to see its
|
||||||
|
containers.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">
|
||||||
|
Containers
|
||||||
|
{data && (
|
||||||
|
<span className="text-secondary fw-normal ms-2">
|
||||||
|
{data.summary.running}/{data.summary.total} running
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div className="card-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-secondary"
|
||||||
|
onClick={() => selected && loadContainers(selected.id)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Refreshing…" : "Refresh"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
{data?.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>
|
||||||
|
))}
|
||||||
|
{data?.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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useState, type CSSProperties } from "react";
|
import { useEffect, useState, type CSSProperties } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
type CurrentUser,
|
type CurrentUser,
|
||||||
type DockhandContainer,
|
|
||||||
type DockhandContainersResponse,
|
|
||||||
type GiteaRepo,
|
type GiteaRepo,
|
||||||
type IntegrationSummary,
|
type IntegrationSummary,
|
||||||
type IntegrationType,
|
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) {
|
function runStatusBadge(repo: GiteaRepo) {
|
||||||
const run = repo.latestRun;
|
const run = repo.latestRun;
|
||||||
if (!run) return <span className="badge bg-secondary-lt text-secondary">No runs</span>;
|
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 [loadingRepos, setLoadingRepos] = useState(false);
|
||||||
const [rerunningRun, setRerunningRun] = useState<number | null>(null);
|
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 [semaphoreData, setSemaphoreData] = useState<SemaphoreTemplatesResponse | null>(null);
|
||||||
const [semaphoreError, setSemaphoreError] = useState<string | null>(null);
|
const [semaphoreError, setSemaphoreError] = useState<string | null>(null);
|
||||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||||
@@ -214,16 +192,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
|||||||
.finally(() => setLoadingRepos(false));
|
.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(() => {
|
useEffect(() => {
|
||||||
if (selected?.type === "tailscale" && selected.enabled && !managing) {
|
if (selected?.type === "tailscale" && selected.enabled && !managing) {
|
||||||
loadTailscaleDevices(selected.id);
|
loadTailscaleDevices(selected.id);
|
||||||
@@ -235,11 +203,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
|||||||
} else {
|
} else {
|
||||||
setGiteaRepos(null);
|
setGiteaRepos(null);
|
||||||
}
|
}
|
||||||
if (selected?.type === "dockhand" && selected.enabled && !managing) {
|
|
||||||
loadDockhandContainers(selected.id);
|
|
||||||
} else {
|
|
||||||
setDockhandData(null);
|
|
||||||
}
|
|
||||||
if (selected?.type === "semaphore" && selected.enabled && !managing) {
|
if (selected?.type === "semaphore" && selected.enabled && !managing) {
|
||||||
loadSemaphoreTemplates(selected.id);
|
loadSemaphoreTemplates(selected.id);
|
||||||
} else {
|
} 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) {
|
async function runTemplate(t: SemaphoreTemplate) {
|
||||||
if (!selectedId) return;
|
if (!selectedId) return;
|
||||||
if (!confirm(`Run "${t.name}" now?`)) return;
|
if (!confirm(`Run "${t.name}" now?`)) return;
|
||||||
@@ -650,94 +598,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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" ? (
|
) : selected?.type === "semaphore" ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@@ -995,6 +855,12 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
||||||
<div className="card-body text-secondary">
|
<div className="card-body text-secondary">
|
||||||
|
|||||||
Reference in new issue
Block a user