221 lines
6.3 KiB
TypeScript
221 lines
6.3 KiB
TypeScript
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) =>
|
||
(versions[f.id] ?? []).map((v, i) => (
|
||
<Table.Tr key={v.id}>
|
||
<Table.Td>
|
||
{i === 0 && (
|
||
<>
|
||
<strong>{f.name}</strong>{" "}
|
||
<Text size="xs" c="dimmed" component="span">
|
||
{f.source_path ? `(${f.source_path})` : "(API)"}
|
||
</Text>
|
||
</>
|
||
)}
|
||
</Table.Td>
|
||
<Table.Td>v{v.version_no}</Table.Td>
|
||
<Table.Td>
|
||
{v.deployed_at ? (
|
||
<StatusBadge status="deployed" />
|
||
) : (
|
||
<StatusBadge status="pending" />
|
||
)}
|
||
</Table.Td>
|
||
<Table.Td>{v.author || "–"}</Table.Td>
|
||
<Table.Td>
|
||
<Button
|
||
size="compact-sm"
|
||
variant="light"
|
||
onClick={() => void onDeploy(f.id, v.version_no)}
|
||
>
|
||
Deployen
|
||
</Button>
|
||
</Table.Td>
|
||
</Table.Tr>
|
||
)),
|
||
)}
|
||
</Table.Tbody>
|
||
</Table>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|