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:
Jonas Miederer 2026-09-06 23:13:05 +00:00
parent 6c195bc2e7
commit 0a2b73f3b4
8 changed files with 618 additions and 4 deletions

View file

@ -0,0 +1,118 @@
import { useCallback, useEffect, useState } from "react";
import {
ApiError,
LANGFLOW_URL,
api,
type FlowDefinition,
type FlowVersion,
} from "../api";
export default function AdminTab() {
const [flows, setFlows] = useState<FlowDefinition[]>([]);
const [versions, setVersions] = useState<Record<string, FlowVersion[]>>({});
const [langflowId, setLangflowId] = useState("");
const [importName, setImportName] = useState("");
const [error, setError] = useState("");
const [info, setInfo] = useState("");
const load = useCallback(async () => {
try {
const list = await api.listFlows();
setFlows(list);
const all: Record<string, FlowVersion[]> = {};
for (const f of list) {
all[f.id] = await api.listVersions(f.id);
}
setVersions(all);
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
}
}, []);
useEffect(() => {
void load();
}, [load]);
async function onImport() {
setError("");
setInfo("");
try {
const result = await api.importFlow(
langflowId,
importName.trim() ? importName.trim() : undefined,
);
setInfo(
`Importiert: ${result.definition.name} (Version ${result.version.version_no})`,
);
setLangflowId("");
setImportName("");
await load();
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
}
}
async function onDeploy(flowId: string, versionNo: number) {
setError("");
setInfo("");
try {
const version = await api.deployVersion(flowId, versionNo);
setInfo(`Deployed: Version ${version.version_no} (${version.lfx_flow_id})`);
await load();
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
}
}
return (
<div>
<h2>Administration</h2>
{error && <p style={{ color: "crimson" }}>{error}</p>}
{info && <p style={{ color: "green" }}>{info}</p>}
<p>
Flows werden visuell in Langflow entworfen:{" "}
<a href={LANGFLOW_URL} target="_blank" rel="noreferrer">
Langflow öffnen
</a>
</p>
<h3>Import aus Langflow</h3>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<input
value={langflowId}
onChange={(e) => setLangflowId(e.target.value)}
placeholder="Langflow Flow-ID (UUID)"
style={{ flex: 1, fontFamily: "monospace" }}
/>
<input
value={importName}
onChange={(e) => setImportName(e.target.value)}
placeholder="Name (optional)"
/>
<button onClick={() => void onImport()} disabled={!langflowId.trim()}>
Importieren + Deployen
</button>
</div>
<h3>Flows + Versionen</h3>
{flows.length === 0 && <p>Keine Flows vorhanden.</p>}
<ul>
{flows.map((f) => (
<li key={f.id} style={{ marginBottom: 8 }}>
<strong>{f.name}</strong>
<ul>
{(versions[f.id] ?? []).map((v) => (
<li key={v.id}>
v{v.version_no} ·{" "}
{v.deployed_at ? `deployed (${v.lfx_flow_id})` : "nicht deployed"} ·{" "}
{v.author || "–"}{" "}
<button onClick={() => void onDeploy(f.id, v.version_no)}>
Deployen
</button>
</li>
))}
</ul>
</li>
))}
</ul>
</div>
);
}