Simplify Integrations to a plain list + Add button

Flagged back when Proxmox/Synology got their own pages: once all six
integration types had moved to dedicated pages, the "browsing"
dropdown here only ever showed one of six near-identical "go manage
this on its own page" redirects, and its final fallback branch was
dead code (every IntegrationType was already covered). Now that all
the actual data views have moved out, drop that mode entirely.

The page is now just what "Manage integrations" already was: a
sortable, CSV-exportable table of configured integrations (name/type/
status), visible to every role since none of that is sensitive, with
an admin-only "Add integration" button and per-row edit/enable-disable/
delete actions. No more mode toggle, no provider-picker dropdown, no
per-type redirect cards -- the six dedicated pages (and the sidebar
nav that already points at them) are how you actually use each
integration now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-18 19:57:03 +02:00
1 parent 16d64c42d8
commit d5b6c55383
2 files changed
+45 -102

No files matched your search

+5 -2
View File
@@ -80,8 +80,11 @@ All modules from the original plan are built:
start/stop/restart actions, a host filter, and a live
running/total widget.
Add/edit/enable/disable/remove each integration's credential itself
under Integrations → Manage integrations.
The Integrations page itself is now just a list of configured
integrations (name/type/status, visible to every role) with an
admin-only "Add integration" button and edit/enable/disable/delete
actions per row — the six dedicated pages above are where you
actually use each one.
- Every table in the app is click-to-sort on any column (numbers, booleans,
and dates/text sort correctly regardless of how the column formats them)
and has an "Export CSV" button next to it that exports whatever's
+40 -100
View File
@@ -1,5 +1,4 @@
import { useEffect, useState, type CSSProperties } from "react";
import { Link } from "react-router-dom";
import { api, type CurrentUser, type IntegrationSummary, type IntegrationType } from "../api/client";
import IntegrationForm from "../components/IntegrationForm";
import IntegrationEditForm from "../components/IntegrationEditForm";
@@ -24,12 +23,10 @@ function typeBadgeStyle(colors: Record<string, string>, type: IntegrationType):
export default function Integrations({ user }: { user: CurrentUser }) {
const isAdmin = user.role === "admin";
const canEdit = user.role === "admin" || user.role === "operator";
const [integrations, setIntegrations] = useState<IntegrationSummary[] | null>(null);
const [managing, setManaging] = useState(false);
const [adding, setAdding] = useState(false);
const [editingIntegration, setEditingIntegration] = useState<IntegrationSummary | null>(null);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [integrationColors, setIntegrationColors] = useState<Record<string, string>>({});
@@ -40,17 +37,12 @@ export default function Integrations({ user }: { user: CurrentUser }) {
function loadIntegrations() {
api.integrations
.list()
.then((res) => {
setIntegrations(res.integrations);
if (!selectedId && res.integrations.length > 0) setSelectedId(res.integrations[0].id);
})
.then((res) => setIntegrations(res.integrations))
.catch((err) => setError(String(err)));
}
useEffect(loadIntegrations, []);
const selected = integrations?.find((i) => i.id === selectedId) ?? null;
async function toggleEnabled(i: IntegrationSummary) {
try {
await api.integrations.update(i.id, { enabled: !i.enabled });
@@ -64,7 +56,6 @@ export default function Integrations({ user }: { user: CurrentUser }) {
if (!confirm(`Delete integration "${i.name}"?`)) return;
try {
await api.integrations.remove(i.id);
if (selectedId === i.id) setSelectedId(null);
loadIntegrations();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
@@ -85,17 +76,21 @@ export default function Integrations({ user }: { user: CurrentUser }) {
return (
<>
<div className="d-flex align-items-center mb-3">
<div>
<h2 className="page-title mb-0">Integrations</h2>
{isAdmin && (
<button className="btn btn-outline-secondary ms-auto" onClick={() => setManaging((v) => !v)}>
{managing ? "Back to browsing" : "Manage integrations"}
<div className="text-secondary small mt-1">
Tailscale, Proxmox, Synology, Semaphore, Gitea, and Dockhand each get their own page once
connected — add and manage credentials here.
</div>
</div>
{isAdmin && !adding && !editingIntegration && (
<button className="btn btn-primary ms-auto" onClick={() => setAdding(true)}>
Add integration
</button>
)}
</div>
{error && <div className="alert alert-danger">{error}</div>}
{managing ? (
<>
{editingIntegration ? (
<IntegrationEditForm
integration={editingIntegration}
@@ -106,8 +101,17 @@ export default function Integrations({ user }: { user: CurrentUser }) {
onCancel={() => setEditingIntegration(null)}
/>
) : (
<IntegrationForm onCreated={loadIntegrations} onCancel={() => setManaging(false)} />
adding && (
<IntegrationForm
onCreated={() => {
setAdding(false);
loadIntegrations();
}}
onCancel={() => setAdding(false)}
/>
)
)}
<div className="card">
<div className="card-header justify-content-end">
<button className="btn btn-outline-secondary btn-sm" onClick={exportCsv} disabled={!integrations || integrations.length === 0}>
@@ -127,7 +131,7 @@ export default function Integrations({ user }: { user: CurrentUser }) {
direction={sortDir}
onSort={requestSort}
/>
<th className="w-1">Actions</th>
{isAdmin && <th className="w-1">Actions</th>}
</tr>
</thead>
<tbody>
@@ -140,13 +144,20 @@ export default function Integrations({ user }: { user: CurrentUser }) {
</span>
</td>
<td>
{isAdmin ? (
<button
className={`badge ${i.enabled ? "bg-green-lt text-green" : "bg-secondary-lt text-secondary"} border-0`}
onClick={() => toggleEnabled(i)}
>
{i.enabled ? "Enabled" : "Disabled"}
</button>
) : (
<span className={`badge ${i.enabled ? "bg-green-lt text-green" : "bg-secondary-lt text-secondary"}`}>
{i.enabled ? "Enabled" : "Disabled"}
</span>
)}
</td>
{isAdmin && (
<td>
<div className="btn-list flex-nowrap">
<button className="btn btn-sm" onClick={() => setEditingIntegration(i)}>
@@ -157,12 +168,22 @@ export default function Integrations({ user }: { user: CurrentUser }) {
</button>
</div>
</td>
)}
</tr>
))}
{sortedIntegrations?.length === 0 && (
<tr>
<td colSpan={4} className="text-secondary text-center">
<td colSpan={isAdmin ? 4 : 3} className="text-secondary text-center">
No integrations configured yet.
{isAdmin && (
<>
{" "}
<button className="btn btn-link p-0 align-baseline" onClick={() => setAdding(true)}>
Add one
</button>
.
</>
)}
</td>
</tr>
)}
@@ -171,86 +192,5 @@ export default function Integrations({ user }: { user: CurrentUser }) {
</div>
</div>
</>
) : (
<>
{integrations?.length === 0 ? (
<div className="card">
<div className="card-body text-secondary">
No integrations configured yet.
{isAdmin && (
<>
{" "}
<button className="btn btn-link p-0 align-baseline" onClick={() => setManaging(true)}>
Add one
</button>
.
</>
)}
</div>
</div>
) : (
<>
<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} ({TYPE_LABELS[i.type]}){!i.enabled ? " — disabled" : ""}
</option>
))}
</select>
</div>
{selected?.type === "tailscale" ? (
<div className="card">
<div className="card-body text-secondary">
Manage this integration's devices on the <Link to="/tailscale">Tailscale</Link> page.
</div>
</div>
) : selected?.type === "gitea" ? (
<div className="card">
<div className="card-body text-secondary">
Manage this integration's repositories on the <Link to="/gitea">Gitea</Link> page.
</div>
</div>
) : selected?.type === "semaphore" ? (
<div className="card">
<div className="card-body text-secondary">
Manage this integration's templates on the <Link to="/semaphore">Semaphore</Link> page.
</div>
</div>
) : selected?.type === "proxmox" ? (
<div className="card">
<div className="card-body text-secondary">
Manage this integration's VMs &amp; containers on the <Link to="/proxmox">Proxmox</Link> page.
</div>
</div>
) : selected?.type === "synology" ? (
<div className="card">
<div className="card-body text-secondary">
View this integration's storage on the <Link to="/synology">Synology</Link> page.
</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">
{selected ? `${TYPE_LABELS[selected.type]} isn't wired up in the UI yet.` : "Select an integration."}
</div>
</div>
)}
</>
)}
</>
)}
</>
);
}