From 8fb8b50c3b311a10f31464ee6fc3370ea5904935 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 01:43:13 +0000 Subject: [PATCH] Add webhook management UI Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/fbc790d4-9aab-4ce1-b6bc-fd0839357492.jpg --- client/src/App.tsx | 2 + client/src/pages/webhooks.tsx | 730 ++++++++++++++++++++++++++++++++++ 2 files changed, 732 insertions(+) create mode 100644 client/src/pages/webhooks.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index f162980..15ca99d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -14,6 +14,7 @@ import UsersRolesPage from "@/pages/users-roles"; import ProvidersPage from "@/pages/providers"; import SettingsPage from "@/pages/settings"; import OrganizationsPage from "@/pages/organizations"; +import WebhooksPage from "@/pages/webhooks"; import { AuthProvider } from "@/hooks/use-auth"; import { ProtectedRoute } from "@/lib/protected-route"; import { ThemeProvider } from "@/hooks/use-theme"; @@ -32,6 +33,7 @@ function Router() { + diff --git a/client/src/pages/webhooks.tsx b/client/src/pages/webhooks.tsx new file mode 100644 index 0000000..7fd1f8d --- /dev/null +++ b/client/src/pages/webhooks.tsx @@ -0,0 +1,730 @@ +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 } 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 [currentWebhook, setCurrentWebhook] = useState(null); + const [testResult, setTestResult] = 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", selectedOrganization?.id], + queryFn: async () => { + if (!selectedOrganization) return []; + const res = await apiRequest( + "GET", + `/api/webhooks?organizationId=${selectedOrganization.id}` + ); + return await res.json(); + }, + enabled: !!selectedOrganization, + }); + + // Create webhook mutation + const createWebhookMutation = useMutation({ + mutationFn: async (data: any) => { + const res = await apiRequest("POST", "/api/webhooks", { + ...data, + organizationId: selectedOrganization?.id, + }); + return await res.json(); + }, + onSuccess: () => { + toast({ + title: "Webhook created", + description: "The webhook has been created successfully.", + }); + queryClient.invalidateQueries({ + queryKey: ["/api/webhooks", selectedOrganization?.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", selectedOrganization?.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", selectedOrganization?.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", + }); + }, + }); + + 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 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); + }; + + // 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 | null) => { + if (!dateString) return "Never"; + 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} +

+
+
+ + + +
+
+
+ ); +} \ No newline at end of file