kirche-einfach/frontend/src/components/AdminTab.tsx

248 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useState } from "react";
import {
Alert,
Anchor,
Button,
Card,
Group,
Table,
Text,
TextInput,
Title,
} from "@mantine/core";
import {
IconAlertCircle,
IconCheck,
IconExternalLink,
IconRefresh,
IconUpload,
} from "@tabler/icons-react";
import {
ApiError,
LANGFLOW_URL,
api,
type FlowDefinition,
type FlowVersion,
} from "../api";
import FlowHelp from "./FlowHelp";
import StatusBadge from "./StatusBadge";
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));
}
}
async function onSync() {
setError("");
setInfo("");
try {
const r = await api.syncFlows();
const parts = [
`Langflow: ${r.pulled.length}`,
`+${r.added.length}`,
`~${r.updated.length}`,
`-${r.deleted.length}`,
].join(" ");
const errs = Object.entries(r.errors)
.map(([f, e]) => `${f}: ${e}`)
.join("; ");
setInfo(`Sync ${parts}${errs ? ` — Fehler: ${errs}` : ""}`);
await load();
} catch (e) {
setError(e instanceof ApiError ? `${e.status}: ${e.message}` : String(e));
}
}
return (
<div>
<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>
)}
<Card withBorder padding="md" mb="md">
<Text mb="xs">
Flows werden visuell in Langflow entworfen:{" "}
<Anchor href={LANGFLOW_URL} target="_blank" rel="noreferrer">
Langflow öffnen <IconExternalLink size={14} />
</Anchor>
</Text>
<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" } }}
/>
<TextInput
label="Name (optional)"
value={importName}
onChange={(e) => setImportName(e.target.value)}
/>
<Button
leftSection={<IconUpload size={16} />}
onClick={() => void onImport()}
disabled={!langflowId.trim()}
>
Importieren + Deployen
</Button>
</Group>
</Card>
<FlowHelp />
<Card withBorder padding="md" mb="md">
<Group>
<Button
variant="light"
leftSection={<IconRefresh size={16} />}
onClick={() => void onSync()}
>
Aus Langflow synchronisieren
</Button>
</Group>
<Text size="sm" c="dimmed" mt="xs">
Holt alle Flows aus Langflow nach backend/flows und deployed sie
danach automatisch.
</Text>
</Card>
{flows.length === 0 ? (
<Text c="dimmed">Keine Flows vorhanden.</Text>
) : (
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Flow</Table.Th>
<Table.Th>Version</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Autor</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{flows.map((f) => {
const list = versions[f.id] ?? [];
const latest = list[list.length - 1];
if (!latest) {
return (
<Table.Tr key={f.id}>
<Table.Td>
<strong>{f.name}</strong>{" "}
<Text size="xs" c="dimmed" component="span">
{f.source_path ? `(${f.source_path})` : "(API)"}
</Text>
</Table.Td>
<Table.Td>–</Table.Td>
<Table.Td>
<StatusBadge status="pending" />
</Table.Td>
<Table.Td>–</Table.Td>
<Table.Td />
</Table.Tr>
);
}
const live = [...list]
.reverse()
.find((v) => v.deployed_at && v.id !== latest.id);
return (
<Table.Tr key={latest.id}>
<Table.Td>
<strong>{f.name}</strong>{" "}
<Text size="xs" c="dimmed" component="span">
{f.source_path ? `(${f.source_path})` : "(API)"}
</Text>
</Table.Td>
<Table.Td>
v{latest.version_no}
{live && (
<Text size="xs" c="dimmed" component="span">
{" "}
(live: v{live.version_no})
</Text>
)}
</Table.Td>
<Table.Td>
{latest.deployed_at ? (
<StatusBadge status="deployed" />
) : (
<StatusBadge status="pending" />
)}
</Table.Td>
<Table.Td>{latest.author || "–"}</Table.Td>
<Table.Td>
<Button
size="compact-sm"
variant="light"
onClick={() => void onDeploy(f.id, latest.version_no)}
>
Deployen
</Button>
</Table.Td>
</Table.Tr>
);
})}
</Table.Tbody>
</Table>
)}
</div>
);
}