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:
1 parent
16d64c42d8
commit
d5b6c55383
2 files changed
+45
-102
No files matched your search
@@ -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
@@ -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 & 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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user