- 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
118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
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>
|
||
);
|
||
}
|