refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter
This commit is contained in:
parent
06ab25fb47
commit
03af41e837
74 changed files with 6089 additions and 5183 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue