feat(ui): webhooks as cards with events in words, calendar sync without env jargon
- Webhooks: one card per connection, events by their German label instead of keys like "product.stock_changed", state as icon + word, a one-sentence explanation of what a webhook is for, retry on errors. - Calendar sync: the group id field says where to find it; the "not configured" note says who sets it up instead of listing environment variable names. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
a21b1665e7
commit
549a75988e
2 files changed
+74
-75
No files matched your search
@@ -87,13 +87,18 @@ function SettingsForm({ settings }: { settings: CalendarSyncSettings }) {
|
||||
<Label htmlFor="sync-enabled">Synchronisation aktiv</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="sync-group-id">Objekt-ID der M365-Gruppe</Label>
|
||||
<Label htmlFor="sync-group-id">Kennung des Microsoft-365-Gruppenkalenders</Label>
|
||||
<Input
|
||||
id="sync-group-id"
|
||||
value={groupId}
|
||||
onChange={(event) => setGroupId(event.target.value)}
|
||||
placeholder="z. B. 3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
||||
aria-describedby="sync-group-id-hint"
|
||||
/>
|
||||
<p id="sync-group-id-hint" className="text-xs text-muted-foreground">
|
||||
Steht im Microsoft-Entra-Admin-Center unter Gruppen → gewünschte Gruppe →
|
||||
„Objekt-ID“. Bei Unklarheit die IT fragen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">Welche Einträge übertragen werden</p>
|
||||
@@ -148,9 +153,9 @@ function StatusSection() {
|
||||
<p className="text-muted-foreground">Lädt…</p>
|
||||
) : !status?.configured ? (
|
||||
<p className="text-muted-foreground">
|
||||
Nicht konfiguriert - die SSO-Umgebungsvariablen (OIDC_ISSUER_URL/OIDC_CLIENT_ID/
|
||||
OIDC_CLIENT_SECRET) fehlen, oder die App-Registrierung hat noch keine
|
||||
Application-Berechtigung „Calendars.ReadWrite“.
|
||||
Noch nicht eingerichtet. Das erledigt die Person, die den Server betreut: Microsoft-
|
||||
Anmeldung (SSO) einrichten und der App die Berechtigung „Calendars.ReadWrite“ geben
|
||||
(Details in der Betriebsanleitung, ARCHITECTURE.md Abschnitt 6.9).
|
||||
</p>
|
||||
) : !canRunNow ? (
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { CircleCheck, CircleSlash, Webhook } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { deleteWebhook, fetchWebhooks } from "@/lib/webhooks/api";
|
||||
import { WEBHOOK_EVENT_LABELS } from "@/lib/webhooks/labels";
|
||||
import { errorMessage } from "@/lib/forms/error-message";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ConfirmDialog } from "@/components/confirm-dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { QueryState } from "@/components/query-state";
|
||||
import { CreateWebhookDialog } from "./create-webhook-dialog";
|
||||
import { EditWebhookDialog } from "./edit-webhook-dialog";
|
||||
|
||||
/** One card per connection - events in words, not technical keys. */
|
||||
export function WebhooksTab() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: webhooks, isLoading, isError } = useQuery({
|
||||
const webhooksQuery = useQuery({
|
||||
queryKey: ["webhooks"],
|
||||
queryFn: fetchWebhooks,
|
||||
});
|
||||
@@ -29,76 +27,72 @@ export function WebhooksTab() {
|
||||
void queryClient.invalidateQueries({ queryKey: ["webhooks"] });
|
||||
toast.success("Webhook gelöscht");
|
||||
},
|
||||
onError: () => toast.error("Webhook konnte nicht gelöscht werden"),
|
||||
onError: (error) => toast.error(errorMessage(error, "Webhook konnte nicht gelöscht werden")),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="max-w-prose text-sm text-muted-foreground">
|
||||
Ein Webhook meldet einem anderen System sofort, wenn hier etwas passiert – zum Beispiel
|
||||
eine neue Ernte. Nur nötig, wenn ein anderes Programm darauf wartet.
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<CreateWebhookDialog />
|
||||
</div>
|
||||
|
||||
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||
{isError ? (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Webhooks konnten nicht geladen werden.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{webhooks && webhooks.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Noch keine Webhooks angelegt.</p>
|
||||
) : null}
|
||||
|
||||
{webhooks && webhooks.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>URL</TableHead>
|
||||
<TableHead>Ereignisse</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Aktion</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{webhooks.map((webhook) => (
|
||||
<TableRow key={webhook.id}>
|
||||
<TableCell className="max-w-64 truncate font-mono text-xs">{webhook.url}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{webhook.events.map((event) => (
|
||||
<Badge key={event} variant="secondary">
|
||||
{event}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={webhook.active ? "default" : "outline"}>
|
||||
{webhook.active ? "Aktiv" : "Inaktiv"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<EditWebhookDialog webhook={webhook} />
|
||||
<ConfirmDialog
|
||||
triggerLabel="Löschen"
|
||||
triggerSize="sm"
|
||||
triggerDisabled={deleteMutation.isPending}
|
||||
title="Webhook löschen?"
|
||||
description={`Der Webhook für „${webhook.url}“ wird endgültig gelöscht. Externe Systeme erhalten danach keine Ereignisse mehr über diese Verbindung, und der Vorgang kann nicht rückgängig gemacht werden.`}
|
||||
confirmLabel="Endgültig löschen"
|
||||
isPending={deleteMutation.isPending}
|
||||
onConfirm={() => deleteMutation.mutate(webhook.id)}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : null}
|
||||
<QueryState
|
||||
query={webhooksQuery}
|
||||
what="Webhooks"
|
||||
empty={<EmptyState icon={Webhook} title="Noch keine Webhooks angelegt" />}
|
||||
>
|
||||
{(webhooks) => (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{webhooks.map((webhook) => (
|
||||
<li
|
||||
key={webhook.id}
|
||||
className="flex flex-col gap-3 rounded-xl border border-border/60 bg-card p-3 sm:flex-row sm:items-start"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<span className="break-all font-mono text-xs">{webhook.url}</span>
|
||||
<span
|
||||
className={
|
||||
webhook.active
|
||||
? "inline-flex items-center gap-1 text-xs font-medium"
|
||||
: "inline-flex items-center gap-1 text-xs text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{webhook.active ? (
|
||||
<CircleCheck className="size-3.5" aria-hidden />
|
||||
) : (
|
||||
<CircleSlash className="size-3.5" aria-hidden />
|
||||
)}
|
||||
{webhook.active ? "Aktiv" : "Pausiert"}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{webhook.events.map((event) => (
|
||||
<Badge key={event} variant="secondary">
|
||||
{WEBHOOK_EVENT_LABELS[event] ?? event}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<EditWebhookDialog webhook={webhook} />
|
||||
<ConfirmDialog
|
||||
triggerLabel="Löschen"
|
||||
triggerDisabled={deleteMutation.isPending}
|
||||
title="Webhook löschen?"
|
||||
description={`Der Webhook für „${webhook.url}“ wird endgültig gelöscht. Externe Systeme erhalten danach keine Ereignisse mehr über diese Verbindung, und der Vorgang kann nicht rückgängig gemacht werden.`}
|
||||
confirmLabel="Endgültig löschen"
|
||||
isPending={deleteMutation.isPending}
|
||||
onConfirm={() => deleteMutation.mutate(webhook.id)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</QueryState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user