kirche-einfach/frontend/src/components/AdminTab.tsx
Jonas Miederer 0a2b73f3b4 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
2026-09-06 23:13:05 +00:00

118 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}