'use client' import { useState } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Switch } from '@/components/ui/switch' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { ArrowLeft, Plus, Pencil, Trash2, Loader2, Webhook, Send, ChevronDown, ChevronUp, Copy, Eye, EyeOff, RefreshCw, CheckCircle2, XCircle, } from 'lucide-react' import { toast } from 'sonner' import { formatDate } from '@/lib/utils' interface WebhookFormData { name: string url: string events: string[] headers: Array<{ key: string; value: string }> maxRetries: number } const defaultForm: WebhookFormData = { name: '', url: '', events: [], headers: [], maxRetries: 3, } export default function WebhooksPage() { const [dialogOpen, setDialogOpen] = useState(false) const [editingId, setEditingId] = useState(null) const [deleteId, setDeleteId] = useState(null) const [formData, setFormData] = useState(defaultForm) const [expandedWebhook, setExpandedWebhook] = useState(null) const [revealedSecrets, setRevealedSecrets] = useState>(new Set()) const [deliveryPage, setDeliveryPage] = useState(1) const utils = trpc.useUtils() const { data: webhooks, isLoading } = trpc.webhook.list.useQuery() const { data: availableEvents } = trpc.webhook.getAvailableEvents.useQuery() const { data: deliveryLog, isLoading: loadingDeliveries } = trpc.webhook.getDeliveryLog.useQuery( { webhookId: expandedWebhook!, page: deliveryPage, pageSize: 10 }, { enabled: !!expandedWebhook } ) const createMutation = trpc.webhook.create.useMutation({ onSuccess: () => { utils.webhook.list.invalidate() toast.success('Webhook created') closeDialog() }, onError: (e) => toast.error(e.message), }) const updateMutation = trpc.webhook.update.useMutation({ onSuccess: () => { utils.webhook.list.invalidate() toast.success('Webhook updated') closeDialog() }, onError: (e) => toast.error(e.message), }) const deleteMutation = trpc.webhook.delete.useMutation({ onSuccess: () => { utils.webhook.list.invalidate() toast.success('Webhook deleted') setDeleteId(null) }, onError: (e) => toast.error(e.message), }) const testMutation = trpc.webhook.test.useMutation({ onSuccess: (data) => { const status = (data as Record)?.status if (status === 'DELIVERED') { toast.success('Test webhook delivered successfully') } else { toast.error(`Test delivery status: ${String(status || 'unknown')}`) } }, onError: (e) => toast.error(e.message), }) const regenerateSecretMutation = trpc.webhook.regenerateSecret.useMutation({ onSuccess: () => { utils.webhook.list.invalidate() toast.success('Secret regenerated') }, onError: (e) => toast.error(e.message), }) const closeDialog = () => { setDialogOpen(false) setEditingId(null) setFormData(defaultForm) } const openEdit = (webhook: Record) => { setEditingId(String(webhook.id)) const headers = webhook.headers as Array<{ key: string; value: string }> | undefined setFormData({ name: String(webhook.name || ''), url: String(webhook.url || ''), events: Array.isArray(webhook.events) ? webhook.events.map(String) : [], headers: Array.isArray(headers) ? headers : [], maxRetries: Number(webhook.maxRetries || 3), }) setDialogOpen(true) } const toggleEvent = (event: string) => { setFormData((prev) => ({ ...prev, events: prev.events.includes(event) ? prev.events.filter((e) => e !== event) : [...prev.events, event], })) } const addHeader = () => { setFormData((prev) => ({ ...prev, headers: [...prev.headers, { key: '', value: '' }], })) } const removeHeader = (index: number) => { setFormData((prev) => ({ ...prev, headers: prev.headers.filter((_, i) => i !== index), })) } const updateHeader = (index: number, field: 'key' | 'value', value: string) => { setFormData((prev) => ({ ...prev, headers: prev.headers.map((h, i) => i === index ? { ...h, [field]: value } : h ), })) } const handleSubmit = () => { if (!formData.name || !formData.url || formData.events.length === 0) { toast.error('Please fill in name, URL, and select at least one event') return } const payload = { name: formData.name, url: formData.url, events: formData.events, headers: formData.headers.filter((h) => h.key) as Record[] | undefined, maxRetries: formData.maxRetries, } if (editingId) { updateMutation.mutate({ id: editingId, ...payload }) } else { createMutation.mutate(payload) } } const copySecret = (secret: string) => { navigator.clipboard.writeText(secret) toast.success('Secret copied to clipboard') } const toggleSecretVisibility = (id: string) => { setRevealedSecrets((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const toggleDeliveryLog = (webhookId: string) => { if (expandedWebhook === webhookId) { setExpandedWebhook(null) } else { setExpandedWebhook(webhookId) setDeliveryPage(1) } } const events = availableEvents || [] const isPending = createMutation.isPending || updateMutation.isPending return (
{/* Header */}

Webhooks

Configure webhook endpoints for platform events

!open && closeDialog()}> {editingId ? 'Edit Webhook' : 'Add Webhook'} Configure a webhook endpoint to receive platform events.
setFormData({ ...formData, name: e.target.value })} />
setFormData({ ...formData, url: e.target.value })} />
{(events as string[]).map((event) => (
toggleEvent(event)} />
))}
{formData.headers.map((header, i) => (
updateHeader(i, 'key', e.target.value)} className="flex-1" /> updateHeader(i, 'value', e.target.value)} className="flex-1" />
))}
setFormData({ ...formData, maxRetries: parseInt(e.target.value) || 0 }) } />
{editingId && (
> | undefined)?.find( (w) => String(w.id) === editingId )?.isActive !== false } onCheckedChange={(checked) => { updateMutation.mutate({ id: editingId, isActive: checked }) }} />
)}
{/* Webhooks list */} {isLoading ? ( ) : webhooks && (webhooks as unknown[]).length > 0 ? (
{(webhooks as Array>).map((webhook) => { const webhookId = String(webhook.id) const webhookEvents = Array.isArray(webhook.events) ? webhook.events : [] const isActive = Boolean(webhook.isActive) const secret = String(webhook.secret || '') const isRevealed = revealedSecrets.has(webhookId) const isExpanded = expandedWebhook === webhookId const recentDelivered = Number(webhook.recentDelivered || 0) const recentFailed = Number(webhook.recentFailed || 0) const deliveryCount = webhook._count ? Number((webhook._count as Record).deliveries || 0) : 0 return (
{String(webhook.name)} {isActive ? ( Active ) : ( Inactive )} {String(webhook.url)}
{webhookEvents.length} events
{recentDelivered} {recentFailed}
{/* Events */}
{webhookEvents.map((event: unknown) => ( {String(event)} ))}
{/* Secret */} {secret && (
Secret: {isRevealed ? secret : '****************************'}
)} {/* Actions */}
{/* Delivery log */} {deliveryCount > 0 && ( toggleDeliveryLog(webhookId)} >
{loadingDeliveries ? (
{[1, 2, 3].map((i) => ( ))}
) : deliveryLog && deliveryLog.items.length > 0 ? ( <> Timestamp Event Status Response Attempts {(deliveryLog.items as Array>).map( (delivery, i) => ( {delivery.createdAt ? formatDate(delivery.createdAt as string | Date) : ''} {String(delivery.event || '')} {delivery.status === 'DELIVERED' ? ( OK ) : delivery.status === 'PENDING' ? ( Pending ) : ( Failed )} {String(delivery.responseStatus || '-')} {String(delivery.attempts || 0)} ) )}
{deliveryLog.totalPages > 1 && (
Page {deliveryLog.page} of {deliveryLog.totalPages}
)} ) : (
No deliveries recorded yet.
)}
)}
) })}
) : (

No webhooks configured

Add a webhook to receive real-time notifications about platform events.

)} {/* Delete confirmation */} setDeleteId(null)}> Delete Webhook Are you sure you want to delete this webhook? All delivery history will be lost. Cancel deleteId && deleteMutation.mutate({ id: deleteId })} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {deleteMutation.isPending && } Delete
) } function WebhooksSkeleton() { return (
{[1, 2].map((i) => (
))}
) }