import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { queryClient, apiRequest } from "@/lib/queryClient"; import { useAuth } from "@/hooks/use-auth"; import { useOrganization } from "@/context/organization-context"; import { Webhook, WebhookDeliveryLog } from "@shared/schema"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { Loader2, Plus, Trash, Edit, ExternalLink, RefreshCw, Clock, Check, X, Activity, Eye, Bell, BellRing } from "lucide-react"; export default function WebhooksPage() { const { toast } = useToast(); const { user } = useAuth(); const { currentOrganization } = useOrganization(); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [isTestResultOpen, setIsTestResultOpen] = useState(false); const [isHistorySheetOpen, setIsHistorySheetOpen] = useState(false); const [isLogsDialogOpen, setIsLogsDialogOpen] = useState(false); const [currentWebhook, setCurrentWebhook] = useState(null); const [currentWebhookId, setCurrentWebhookId] = useState(null); const [testResult, setTestResult] = useState(null); const [selectedLog, setSelectedLog] = useState(null); // Form state for create/edit const [formData, setFormData] = useState({ name: "", url: "", events: ["dns.*"], secret: "", isActive: true, }); // Reset form when dialog closes const resetForm = () => { setFormData({ name: "", url: "", events: ["dns.*"], secret: "", isActive: true, }); }; // Fetch webhooks for the selected organization const { data: webhooks = [], isLoading, error, } = useQuery({ queryKey: ["/api/webhooks", currentOrganization?.id], queryFn: async () => { if (!currentOrganization) return []; const res = await apiRequest( "GET", `/api/webhooks?organizationId=${currentOrganization.id}` ); return await res.json(); }, enabled: !!currentOrganization, }); // Create webhook mutation const createWebhookMutation = useMutation({ mutationFn: async (data: any) => { const res = await apiRequest("POST", "/api/webhooks", { ...data, organizationId: currentOrganization?.id, }); return await res.json(); }, onSuccess: () => { toast({ title: "Webhook created", description: "The webhook has been created successfully.", }); queryClient.invalidateQueries({ queryKey: ["/api/webhooks", currentOrganization?.id], }); setIsCreateOpen(false); resetForm(); }, onError: (error: Error) => { toast({ title: "Error creating webhook", description: error.message, variant: "destructive", }); }, }); // Update webhook mutation const updateWebhookMutation = useMutation({ mutationFn: async ({ id, data }: { id: string; data: any }) => { const res = await apiRequest("PUT", `/api/webhooks/${id}`, data); return await res.json(); }, onSuccess: () => { toast({ title: "Webhook updated", description: "The webhook has been updated successfully.", }); queryClient.invalidateQueries({ queryKey: ["/api/webhooks", currentOrganization?.id], }); setIsEditOpen(false); resetForm(); }, onError: (error: Error) => { toast({ title: "Error updating webhook", description: error.message, variant: "destructive", }); }, }); // Delete webhook mutation const deleteWebhookMutation = useMutation({ mutationFn: async (id: string) => { await apiRequest("DELETE", `/api/webhooks/${id}`); }, onSuccess: () => { toast({ title: "Webhook deleted", description: "The webhook has been deleted successfully.", }); queryClient.invalidateQueries({ queryKey: ["/api/webhooks", currentOrganization?.id], }); setIsDeleteOpen(false); setCurrentWebhook(null); }, onError: (error: Error) => { toast({ title: "Error deleting webhook", description: error.message, variant: "destructive", }); }, }); // Test webhook mutation const testWebhookMutation = useMutation({ mutationFn: async (id: string) => { const res = await apiRequest("POST", `/api/webhooks/${id}/test`); return await res.json(); }, onSuccess: (data) => { setTestResult({ success: true, message: data.message || "Webhook test successful", timestamp: new Date().toISOString(), }); setIsTestResultOpen(true); toast({ title: "Webhook tested", description: "The webhook has been tested successfully.", }); }, onError: (error: Error) => { setTestResult({ success: false, message: error.message || "Webhook test failed", timestamp: new Date().toISOString(), }); setIsTestResultOpen(true); toast({ title: "Error testing webhook", description: error.message, variant: "destructive", }); }, }); // Retry webhook delivery mutation const retryWebhookDeliveryMutation = useMutation({ mutationFn: async (logId: string) => { const res = await apiRequest("POST", `/api/webhook-logs/${logId}/retry`); return await res.json(); }, onSuccess: (data) => { toast({ title: "Webhook delivery retried", description: "The webhook delivery has been retried successfully.", }); // Refresh the logs to show the new retry attempt queryClient.invalidateQueries({ queryKey: ["/api/webhooks", currentWebhookId, "logs"], }); // Reset the selected log setSelectedLog(null); }, onError: (error: Error) => { toast({ title: "Error retrying webhook", description: error.message, variant: "destructive", }); }, }); // Fetch webhook delivery logs const { data: webhookLogs = [], isLoading: isLogsLoading, error: logsError, } = useQuery({ queryKey: ["/api/webhooks", currentWebhookId, "logs"], queryFn: async () => { if (!currentWebhookId) return []; const res = await apiRequest("GET", `/api/webhooks/${currentWebhookId}/logs`); return await res.json(); }, enabled: !!currentWebhookId && isLogsDialogOpen, }); const handleCreateSubmit = (e: React.FormEvent) => { e.preventDefault(); createWebhookMutation.mutate(formData); }; const handleEditSubmit = (e: React.FormEvent) => { e.preventDefault(); if (currentWebhook) { updateWebhookMutation.mutate({ id: currentWebhook.id, data: formData, }); } }; const handleDelete = () => { if (currentWebhook) { deleteWebhookMutation.mutate(currentWebhook.id); } }; const handleTestWebhook = (id: string) => { testWebhookMutation.mutate(id); }; const handleRetryWebhook = (logId: string) => { retryWebhookDeliveryMutation.mutate(logId); }; const handleEditClick = (webhook: Webhook) => { setCurrentWebhook(webhook); setFormData({ name: webhook.name, url: webhook.url, events: webhook.events, secret: webhook.secret || "", isActive: webhook.isActive, }); setIsEditOpen(true); }; const handleDeleteClick = (webhook: Webhook) => { setCurrentWebhook(webhook); setIsDeleteOpen(true); }; const handleViewLogs = (webhookId: string) => { setCurrentWebhookId(webhookId); const webhook = webhooks.find((w: Webhook) => w.id === webhookId); if (webhook) { setCurrentWebhook(webhook); } setIsLogsDialogOpen(true); }; // Event options for the select component const eventOptions = [ { label: "All DNS events", value: "dns.*" }, { label: "DNS create events", value: "dns.create" }, { label: "DNS update events", value: "dns.update" }, { label: "DNS delete events", value: "dns.delete" }, ]; // Helper to format the date const formatDate = (dateString: string | Date | null | undefined) => { if (!dateString) return "Never"; if (dateString instanceof Date) { return dateString.toLocaleString(); } return new Date(dateString).toLocaleString(); }; if (isLoading) { return (
); } if (error) { return (
Error loading webhooks

{(error as Error).message}

); } return (

Webhooks

Manage notifications for DNS changes

Webhooks Receive notifications when DNS records change {webhooks.length === 0 ? (

No webhooks configured

Webhooks allow your systems to be notified when DNS records change. Add a webhook to get started.

) : (
Name URL Events Status Last Triggered Actions {webhooks.map((webhook: Webhook) => ( {webhook.name} {webhook.url}
{webhook.events.map((event) => ( {event} ))}
{webhook.isActive ? ( Active ) : ( Inactive )} {webhook.lastTriggered ? (
{formatDate(webhook.lastTriggered)}
) : ( "Never" )}
))}
)}
{/* Create Webhook Dialog */} Create Webhook Add a new webhook to receive notifications when DNS records change.
setFormData({ ...formData, name: e.target.value }) } placeholder="Production DNS Alerts" required />
setFormData({ ...formData, url: e.target.value }) } placeholder="https://example.com/webhook" required />

The URL that will receive webhook events

{eventOptions.map((option) => (
{ if (checked) { setFormData({ ...formData, events: [...formData.events, option.value], }); } else { setFormData({ ...formData, events: formData.events.filter( (e) => e !== option.value ), }); } }} />
))}
setFormData({ ...formData, secret: e.target.value }) } placeholder="webhook_secret_key" />

Used to sign webhook payloads so you can verify they came from us

setFormData({ ...formData, isActive: checked }) } />
{/* Edit Webhook Dialog */} Edit Webhook Update the webhook configuration.
setFormData({ ...formData, name: e.target.value }) } placeholder="Production DNS Alerts" required />
setFormData({ ...formData, url: e.target.value }) } placeholder="https://example.com/webhook" required />
{eventOptions.map((option) => (
{ if (checked) { setFormData({ ...formData, events: [...formData.events, option.value], }); } else { setFormData({ ...formData, events: formData.events.filter( (e) => e !== option.value ), }); } }} />
))}
setFormData({ ...formData, secret: e.target.value }) } placeholder="webhook_secret_key" />

Used to sign webhook payloads so you can verify they came from us

setFormData({ ...formData, isActive: checked }) } />
{/* Delete Webhook Dialog */} Are you sure? This action cannot be undone. This will permanently delete the webhook{" "} {currentWebhook?.name} and stop all notifications. setCurrentWebhook(null)}> Cancel {deleteWebhookMutation.isPending && ( )} Delete {/* Test Result Dialog */} Test Result Result of the webhook test request.
{testResult?.success ? ( Success ) : ( Failed )} {formatDate(testResult?.timestamp)}

{testResult?.message}

{/* Webhook Delivery Logs Dialog */} { setIsLogsDialogOpen(open); if (!open) { setCurrentWebhookId(null); setSelectedLog(null); } }} > Delivery Logs - {currentWebhook?.name} View webhook delivery history and status {isLogsLoading ? (
) : logsError ? (

Error loading logs

{(logsError as Error).message}

) : webhookLogs.length === 0 ? (

No delivery logs found

This webhook hasn't been triggered yet or logs have been cleared.

) : (
Event Status Time Actions {webhookLogs.map((log: WebhookDeliveryLog) => ( {log.event} {log.status ? ( Success ) : ( Failed )} {formatDate(log.createdAt)} ))}
{selectedLog && (

Delivery Details

{!selectedLog.status && ( )}

Status

{selectedLog.status ? ( Success ({selectedLog.statusCode || 200}) ) : ( Failed {selectedLog.statusCode && `(${selectedLog.statusCode})`} )} {selectedLog.retryCount > 0 && ( Retry {selectedLog.retryCount} )}
{selectedLog.message && (

Message

{selectedLog.message}
)}

Payload

                          {JSON.stringify(selectedLog.payload, null, 2)}
                        
{selectedLog.responseBody && (

Response

                            {selectedLog.responseBody}
                          
)} {selectedLog.signature && (

Signature

{selectedLog.signature}
)}
)}
)}
); }