feat: router with RJSF workflows and access management UI
This commit is contained in:
parent
46ecab00db
commit
62d01b5097
15 changed files with 948 additions and 144 deletions
137
frontend/src/pages/WorkflowsPage.tsx
Normal file
137
frontend/src/pages/WorkflowsPage.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import Form from "@rjsf/core";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
import { ApiError, api, type FlowDefinition, type InputSchema, type Run } from "../api";
|
||||
import { useAuth } from "../auth";
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
const { me } = useAuth();
|
||||
const [flows, setFlows] = useState<FlowDefinition[]>([]);
|
||||
const [flowId, setFlowId] = useState("");
|
||||
const [schema, setSchema] = useState<InputSchema | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
const [freeText, setFreeText] = useState("");
|
||||
const [runs, setRuns] = useState<Run[]>([]);
|
||||
const [result, setResult] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const fail = useCallback((e: unknown) => {
|
||||
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.listFlows()
|
||||
.then((list) => {
|
||||
setFlows(list);
|
||||
const first = list[0];
|
||||
if (first) setFlowId((prev) => prev || first.id);
|
||||
})
|
||||
.catch(fail);
|
||||
}, [fail]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!flowId) return;
|
||||
setError("");
|
||||
setResult("");
|
||||
Promise.all([api.getInputSchema(flowId), api.listRuns(flowId)])
|
||||
.then(([s, r]) => {
|
||||
setSchema(s);
|
||||
setFormData({});
|
||||
setRuns(r);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
// Flows without a deployed version have no schema yet.
|
||||
if (e instanceof ApiError && e.status === 404) {
|
||||
setSchema(null);
|
||||
setRuns([]);
|
||||
setError("Für diesen Flow ist noch keine Version deployed.");
|
||||
} else fail(e);
|
||||
});
|
||||
}, [flowId, fail]);
|
||||
|
||||
async function onRun(input: unknown) {
|
||||
if (!flowId) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setResult("");
|
||||
try {
|
||||
const run = await api.runFlow(flowId, input);
|
||||
setRuns((prev) => [run, ...prev]);
|
||||
setResult(run.output?.result ?? run.output?.error ?? run.status);
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Workflows</h2>
|
||||
{!me && <p>Sie sind nicht angemeldet — Läufe nutzen den Standard-Mandanten.</p>}
|
||||
{error && <p style={{ color: "crimson" }}>{error}</p>}
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||
<select value={flowId} onChange={(e) => setFlowId(e.target.value)}>
|
||||
<option value="">— Flow wählen —</option>
|
||||
{flows.map((f) => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{schema && schema.fallback && (
|
||||
<div>
|
||||
<textarea
|
||||
rows={6}
|
||||
style={{ width: "100%", fontFamily: "monospace" }}
|
||||
value={freeText}
|
||||
onChange={(e) => setFreeText(e.target.value)}
|
||||
placeholder="Freitext-Eingabe"
|
||||
/>
|
||||
<button
|
||||
onClick={() => void onRun({ text: freeText })}
|
||||
disabled={!flowId || busy}
|
||||
>
|
||||
{busy ? "Läuft …" : "Starten"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{schema && !schema.fallback && (
|
||||
<Form
|
||||
schema={schema.json_schema as RJSFSchema}
|
||||
uiSchema={schema.ui_schema as UiSchema}
|
||||
validator={validator}
|
||||
formData={formData}
|
||||
onChange={(e) =>
|
||||
setFormData((e.formData as Record<string, unknown>) ?? {})
|
||||
}
|
||||
onSubmit={(e) => void onRun(e.formData)}
|
||||
>
|
||||
<button type="submit" disabled={busy}>
|
||||
{busy ? "Läuft …" : "Starten"}
|
||||
</button>
|
||||
</Form>
|
||||
)}
|
||||
{result && (
|
||||
<div>
|
||||
<h3>Ergebnis</h3>
|
||||
<pre style={{ whiteSpace: "pre-wrap" }}>{result}</pre>
|
||||
</div>
|
||||
)}
|
||||
<h3>Letzte Läufe</h3>
|
||||
{runs.length === 0 && <p>Noch keine Läufe.</p>}
|
||||
<ul>
|
||||
{runs.map((r) => (
|
||||
<li key={r.id}>
|
||||
<strong>{r.status}</strong> · {r.tenant_id} ·{" "}
|
||||
<code>{r.output?.result ?? r.output?.error ?? "–"}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue