feat: minimal frontend (run view, approvals, admin) with E2E smoke
- 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
This commit is contained in:
parent
6c195bc2e7
commit
0a2b73f3b4
8 changed files with 618 additions and 4 deletions
95
frontend/src/components/ApprovalsTab.tsx
Normal file
95
frontend/src/components/ApprovalsTab.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue