- Tabs: Vorgaenge (run flows + history), Freigaben (decide tasks), Administration (Langflow link, import, deploy) - Typed API client over vite /api proxy; tsc + vite build clean - scripts/smoke_e2e.py: real LFX + API end-to-end, all green - 34 backend tests green
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
||
import { ApiError, api, type ApprovalTask } from "../api";
|
||
|
||
export default function ApprovalsTab() {
|
||
const [tasks, setTasks] = useState<ApprovalTask[]>([]);
|
||
const [approver, setApprover] = useState("");
|
||
const [note, setNote] = useState("");
|
||
const [error, setError] = useState("");
|
||
const [busyId, setBusyId] = useState("");
|
||
|
||
const load = useCallback(async () => {
|
||
try {
|
||
setTasks(await api.listApprovals());
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, [load]);
|
||
|
||
async function decide(taskId: string, decision: "approve" | "reject") {
|
||
setBusyId(taskId);
|
||
setError("");
|
||
try {
|
||
const result = await api.decide(taskId, decision, approver, note);
|
||
setTasks((prev) =>
|
||
prev.map((t) => (t.id === taskId ? result.task : t)),
|
||
);
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||
} finally {
|
||
setBusyId("");
|
||
}
|
||
}
|
||
|
||
const open = tasks.filter((t) => t.status === "open");
|
||
const done = tasks.filter((t) => t.status !== "open");
|
||
|
||
return (
|
||
<div>
|
||
<h2>Freigaben</h2>
|
||
{error && <p style={{ color: "crimson" }}>{error}</p>}
|
||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||
<input
|
||
value={approver}
|
||
onChange={(e) => setApprover(e.target.value)}
|
||
placeholder="Name (Bearbeiter)"
|
||
/>
|
||
<input
|
||
value={note}
|
||
onChange={(e) => setNote(e.target.value)}
|
||
placeholder="Notiz"
|
||
style={{ flex: 1 }}
|
||
/>
|
||
<button onClick={() => void load()}>Aktualisieren</button>
|
||
</div>
|
||
<h3>Offen ({open.length})</h3>
|
||
{open.length === 0 && <p>Keine offenen Freigaben.</p>}
|
||
<ul>
|
||
{open.map((t) => (
|
||
<li key={t.id} style={{ marginBottom: 8 }}>
|
||
<strong>{t.title || t.id}</strong> · {t.tenant_id}
|
||
<br />
|
||
<code>{JSON.stringify(t.payload)}</code>
|
||
<br />
|
||
<button
|
||
onClick={() => void decide(t.id, "approve")}
|
||
disabled={busyId === t.id}
|
||
>
|
||
Freigeben
|
||
</button>{" "}
|
||
<button
|
||
onClick={() => void decide(t.id, "reject")}
|
||
disabled={busyId === t.id}
|
||
>
|
||
Ablehnen
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<h3>Entschieden ({done.length})</h3>
|
||
<ul>
|
||
{done.map((t) => (
|
||
<li key={t.id}>
|
||
<strong>{t.status}</strong> · {t.title || t.id} ·{" "}
|
||
{t.approver ?? "–"}
|
||
{t.decision_note ? ` · „${t.decision_note}“` : ""}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
);
|
||
}
|