import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
createWebhook,
deleteWebhook,
listWebhookDeliveries,
listWebhooks,
testWebhook,
updateWebhook,
type OutboundWebhook,
type WebhookDelivery,
} from '@/lib/api-client';
import { useDemoMode } from '@/lib/demo-mode';
import { formatRelative } from '@/lib/time';
import { DemoRestricted, useToast } from '@/shared/components';
import {
Webhook,
Plus,
Trash2,
Send,
Loader2,
ChevronDown,
ChevronRight,
Copy,
X,
} from 'lucide-react';
const inputClass =
'h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)] transition-all';
const eventSuggestions = ['*', 'service.*', 'project.*', 'database.*', 'deployment.*', 'template.*'];
function statusClass(status: WebhookDelivery['status']): string {
switch (status) {
case 'success':
return 'text-[var(--success)]';
case 'failed':
return 'text-[var(--error)]';
default:
return 'text-[var(--warning)]';
}
}
function WebhookRow({
webhook,
onDelete,
onToggle,
onTest,
testing,
}: {
webhook: OutboundWebhook;
onDelete: () => void;
onToggle: () => void;
onTest: () => void;
testing: boolean;
}) {
const [expanded, setExpanded] = useState(false);
const deliveriesQuery = useQuery({
queryKey: ['webhook-deliveries', webhook.id],
queryFn: () => listWebhookDeliveries(webhook.id),
enabled: expanded,
refetchInterval: expanded ? 10000 : false,
});
return (
{webhook.name}
{!webhook.enabled && (
disabled
)}
{webhook.url}
{webhook.events.map((ev) => (
{ev}
))}
{expanded && (
Recent deliveries
{deliveriesQuery.isLoading ? (
) : (deliveriesQuery.data ?? []).length === 0 ? (
No deliveries yet.
) : (
{(deliveriesQuery.data ?? []).slice(0, 20).map((d) => (
{d.status}
{d.event}
{d.response_status != null && (
HTTP {d.response_status}
)}
{d.attempts} tries
{d.duration_ms != null && (
{d.duration_ms}ms
)}
{formatRelative(d.created_at)}
))}
)}
)}
);
}
export function WebhooksPage() {
const isDemoMode = useDemoMode();
const queryClient = useQueryClient();
const toast = useToast();
const [creating, setCreating] = useState(false);
const [form, setForm] = useState({ name: '', url: '', events: [] as string[] });
const [secret, setSecret] = useState(null);
const [testingId, setTestingId] = useState(null);
const webhooksQuery = useQuery({
queryKey: ['webhooks'],
queryFn: listWebhooks,
enabled: !isDemoMode,
});
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['webhooks'] });
const createMutation = useMutation({
mutationFn: () => createWebhook(form),
onSuccess: (created) => {
invalidate();
setCreating(false);
setForm({ name: '', url: '', events: [] });
if (created.secret) {
setSecret(created.secret);
}
},
onError: (err) => toast.showToast(err instanceof Error ? err.message : 'Failed to create webhook', 'error'),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteWebhook(id),
onSuccess: invalidate,
onError: (err) => toast.showToast(err instanceof Error ? err.message : 'Delete failed', 'error'),
});
const toggleMutation = useMutation({
mutationFn: (w: OutboundWebhook) => updateWebhook(w.id, { enabled: !w.enabled }),
onSuccess: invalidate,
});
const testMutation = useMutation({
mutationFn: (id: string) => testWebhook(id),
onMutate: (id) => setTestingId(id),
onSettled: () => setTestingId(null),
onSuccess: () => toast.showToast('Test ping queued', 'info'),
onError: (err) => toast.showToast(err instanceof Error ? err.message : 'Test failed', 'error'),
});
if (isDemoMode) {
return ;
}
const toggleEvent = (ev: string) =>
setForm((f) => ({
...f,
events: f.events.includes(ev) ? f.events.filter((e) => e !== ev) : [...f.events, ev],
}));
return (
Webhooks_
Signed HTTP delivery for every platform event — deploys, restarts, deletes
{secret && (
Signing secret — shown once
Verify deliveries with X-Containr-Signature
{secret}
)}
{webhooksQuery.isLoading ? (
) : (webhooksQuery.data ?? []).length === 0 ? (
No webhooks yet.
Subscribe an HTTP endpoint to platform events like service.* or{' '}
deployment.fail.
) : (
(webhooksQuery.data ?? []).map((w) => (
testMutation.mutate(w.id)}
onToggle={() => toggleMutation.mutate(w)}
onDelete={() => {
if (window.confirm(`Delete webhook "${w.name}"?`)) {
deleteMutation.mutate(w.id);
}
}}
/>
))
)}
{creating && (
setCreating(false)} />
New webhook
)}
);
}