feat: Mantine design system with dark mode

This commit is contained in:
Jonas Miederer 2026-09-12 08:39:00 +02:00
parent 6e12720ad6
commit 38d4899b07
18 changed files with 1170 additions and 322 deletions

View file

@ -1,4 +1,22 @@
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,
@ -6,6 +24,7 @@ import {
type FlowDefinition,
type FlowVersion,
} from "../api";
import StatusBadge from "./StatusBadge";
export default function AdminTab() {
const [flows, setFlows] = useState<FlowDefinition[]>([]);
@ -86,61 +105,110 @@ export default function AdminTab() {
return (
<div>
<h2>Administration</h2>
{error && <p style={{ color: "crimson" }}>{error}</p>}
{info && <p style={{ color: "green" }}>{info}</p>}
<p>
Flows werden visuell in Langflow entworfen:{" "}
<a href={LANGFLOW_URL} target="_blank" rel="noreferrer">
Langflow öffnen
</a>
</p>
<h3>Import aus Langflow</h3>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<input
value={langflowId}
onChange={(e) => setLangflowId(e.target.value)}
placeholder="Langflow Flow-ID (UUID)"
style={{ flex: 1, fontFamily: "monospace" }}
/>
<input
value={importName}
onChange={(e) => setImportName(e.target.value)}
placeholder="Name (optional)"
/>
<button onClick={() => void onImport()} disabled={!langflowId.trim()}>
Importieren + Deployen
</button>
</div>
<h3>Flows + Versionen</h3>
<div style={{ marginBottom: 12 }}>
<button onClick={() => void onSync()}>
Aus backend/flows synchronisieren
</button>
</div>
{flows.length === 0 && <p>Keine Flows vorhanden.</p>}
<ul>
{flows.map((f) => (
<li key={f.id} style={{ marginBottom: 8 }}>
<strong>{f.name}</strong>{" "}
<span style={{ fontFamily: "monospace", fontSize: "0.85em" }}>
{f.source_path ? `(${f.source_path})` : "(API)"}
</span>
<ul>
{(versions[f.id] ?? []).map((v) => (
<li key={v.id}>
v{v.version_no} ·{" "}
{v.deployed_at ? `deployed (${v.lfx_flow_id})` : "nicht deployed"} ·{" "}
{v.author || "–"}{" "}
<button onClick={() => void onDeploy(f.id, v.version_no)}>
Deployen
</button>
</li>
))}
</ul>
</li>
))}
</ul>
<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>
<Card withBorder padding="md" mb="md">
<Group>
<Button
variant="light"
leftSection={<IconRefresh size={16} />}
onClick={() => void onSync()}
>
Aus backend/flows synchronisieren
</Button>
</Group>
</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>
);
}