182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
||
import Form from "@rjsf/mantine";
|
||
import validator from "@rjsf/validator-ajv8";
|
||
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||
import {
|
||
Alert,
|
||
Button,
|
||
Card,
|
||
Code,
|
||
Select,
|
||
Table,
|
||
Text,
|
||
Textarea,
|
||
Title,
|
||
} from "@mantine/core";
|
||
import { IconAlertCircle, IconPlayerPlay } from "@tabler/icons-react";
|
||
import { ApiError, api, type FlowDefinition, type InputSchema, type Run } from "../api";
|
||
import StatusBadge from "../components/StatusBadge";
|
||
|
||
export default function WorkflowsPage() {
|
||
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>
|
||
<Title order={2} mb="md">
|
||
Workflows
|
||
</Title>
|
||
{error && (
|
||
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
|
||
{error}
|
||
</Alert>
|
||
)}
|
||
<Select
|
||
label="Flow"
|
||
placeholder="— Flow wählen —"
|
||
value={flowId}
|
||
onChange={(v) => setFlowId(v ?? "")}
|
||
data={flows.map((f) => ({ value: f.id, label: f.name }))}
|
||
mb="md"
|
||
maw={480}
|
||
/>
|
||
{schema && schema.fallback && (
|
||
<Card withBorder padding="lg" mb="md">
|
||
<Textarea
|
||
label="Freitext-Eingabe"
|
||
rows={6}
|
||
value={freeText}
|
||
onChange={(e) => setFreeText(e.target.value)}
|
||
mb="sm"
|
||
/>
|
||
<Button
|
||
leftSection={<IconPlayerPlay size={16} />}
|
||
loading={busy}
|
||
disabled={!flowId}
|
||
onClick={() => void onRun({ text: freeText })}
|
||
>
|
||
Starten
|
||
</Button>
|
||
</Card>
|
||
)}
|
||
{schema && !schema.fallback && (
|
||
<Card withBorder padding="lg" mb="md">
|
||
<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"
|
||
leftSection={<IconPlayerPlay size={16} />}
|
||
loading={busy}
|
||
mt="sm"
|
||
>
|
||
Starten
|
||
</Button>
|
||
</Form>
|
||
</Card>
|
||
)}
|
||
{result && (
|
||
<Card withBorder padding="lg" mb="md">
|
||
<Title order={3} mb="xs">
|
||
Ergebnis
|
||
</Title>
|
||
<Code block>{result}</Code>
|
||
</Card>
|
||
)}
|
||
<Title order={3} mb="xs">
|
||
Letzte Läufe
|
||
</Title>
|
||
{runs.length === 0 ? (
|
||
<Text c="dimmed">Noch keine Läufe.</Text>
|
||
) : (
|
||
<Table striped highlightOnHover>
|
||
<Table.Thead>
|
||
<Table.Tr>
|
||
<Table.Th>Status</Table.Th>
|
||
<Table.Th>Mandant</Table.Th>
|
||
<Table.Th>Ergebnis</Table.Th>
|
||
</Table.Tr>
|
||
</Table.Thead>
|
||
<Table.Tbody>
|
||
{runs.map((r) => (
|
||
<Table.Tr key={r.id}>
|
||
<Table.Td>
|
||
<StatusBadge status={r.status} />
|
||
</Table.Td>
|
||
<Table.Td>{r.tenant_id}</Table.Td>
|
||
<Table.Td>
|
||
<Code>{r.output?.result ?? r.output?.error ?? "–"}</Code>
|
||
</Table.Td>
|
||
</Table.Tr>
|
||
))}
|
||
</Table.Tbody>
|
||
</Table>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|