refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter

This commit is contained in:
Jonas Miederer 2026-09-28 18:03:07 +02:00
parent 06ab25fb47
commit 03af41e837
74 changed files with 6089 additions and 5183 deletions

View file

@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import {
Alert,
Anchor,
Button,
Card,
@ -10,83 +9,111 @@ import {
TextInput,
Title,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import {
IconAlertCircle,
IconCheck,
IconExternalLink,
IconRefresh,
IconUpload,
} from "@tabler/icons-react";
import { z } from "zod";
import {
ApiError,
LANGFLOW_URL,
api,
type FlowDefinition,
type FlowVersion,
} from "../api";
import { formatApiError } from "../utils";
import FlowHelp from "./FlowHelp";
import { ErrorAlert } from "./ErrorAlert";
import StatusBadge from "./StatusBadge";
const importSchema = z.object({
langflowId: z.string().min(1, "Flow-ID erforderlich"),
importName: z.string(),
});
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 importForm = useForm({
initialValues: { langflowId: "", importName: "" },
validate: (values) => {
const parsed = importSchema.safeParse(values);
if (parsed.success) return {};
return {
langflowId:
parsed.error.flatten().fieldErrors.langflowId?.[0] ?? null,
};
},
});
const [error, setError] = useState("");
const [info, setInfo] = useState("");
const load = useCallback(async () => {
function notifyInfo(message: string) {
notifications.show({ message, color: "green" });
}
useEffect(() => {
void (async () => {
try {
const list = await api.listFlows();
setFlows(list);
const entries = await Promise.all(
list.map(async (f) => [f.id, await api.listVersions(f.id)] as const),
);
setVersions(Object.fromEntries(entries));
} catch (e) {
setError(formatApiError(e));
}
})();
}, []);
async function load() {
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);
const entries = await Promise.all(
list.map(async (f) => [f.id, await api.listVersions(f.id)] as const),
);
setVersions(Object.fromEntries(entries));
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
setError(formatApiError(e));
}
}, []);
useEffect(() => {
void load();
}, [load]);
}
async function onImport() {
setError("");
setInfo("");
const validation = importForm.validate();
if (validation.hasErrors) return;
const { langflowId, importName } = importForm.values;
try {
const result = await api.importFlow(
langflowId,
importName.trim() ? importName.trim() : undefined,
);
setInfo(
notifyInfo(
`Importiert: ${result.definition.name} (Version ${result.version.version_no})`,
);
setLangflowId("");
setImportName("");
importForm.reset();
await load();
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
setError(formatApiError(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})`);
notifyInfo(`Deployed: Version ${version.version_no} (${version.lfx_flow_id})`);
await load();
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
setError(formatApiError(e));
}
}
async function onSync() {
setError("");
setInfo("");
try {
const r = await api.syncFlows();
const parts = [
@ -98,10 +125,10 @@ export default function AdminTab() {
const errs = Object.entries(r.errors)
.map(([f, e]) => `${f}: ${e}`)
.join("; ");
setInfo(`Sync ${parts}${errs ? ` — Fehler: ${errs}` : ""}`);
notifyInfo(`Sync ${parts}${errs ? ` — Fehler: ${errs}` : ""}`);
await load();
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
setError(formatApiError(e));
}
}
@ -110,16 +137,7 @@ export default function AdminTab() {
<Title order={3} mb="xs">
Flows & Versionen
</Title>
{error && (
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
{error}
</Alert>
)}
{info && (
<Alert color="green" icon={<IconCheck size={18} />} mb="md">
{info}
</Alert>
)}
<ErrorAlert message={error} />
<Card withBorder padding="md" mb="md">
<Text mb="xs">
Flows werden visuell in Langflow entworfen:{" "}
@ -130,20 +148,18 @@ export default function AdminTab() {
<Group align="flex-end">
<TextInput
label="Langflow Flow-ID (UUID)"
value={langflowId}
onChange={(e) => setLangflowId(e.target.value)}
style={{ flex: 1 }}
styles={{ input: { fontFamily: "monospace" } }}
{...importForm.getInputProps("langflowId")}
/>
<TextInput
label="Name (optional)"
value={importName}
onChange={(e) => setImportName(e.target.value)}
{...importForm.getInputProps("importName")}
/>
<Button
leftSection={<IconUpload size={16} />}
onClick={() => void onImport()}
disabled={!langflowId.trim()}
disabled={!importForm.values.langflowId.trim()}
>
Importieren + Deployen
</Button>
@ -200,9 +216,9 @@ export default function AdminTab() {
</Table.Tr>
);
}
const live = [...list]
.reverse()
.find((v) => v.deployed_at && v.id !== latest.id);
const live = list
.filter((v) => v.deployed_at && v.id !== latest.id)
.at(-1);
return (
<Table.Tr key={latest.id}>
<Table.Td>