diff --git a/web/app/(app)/dashboard/(components)/webhooks-history.tsx b/web/app/(app)/dashboard/(components)/webhooks-history.tsx index cdf4002..fb3f6df 100644 --- a/web/app/(app)/dashboard/(components)/webhooks-history.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks-history.tsx @@ -21,6 +21,7 @@ import { Ellipsis, MessageSquare, Smartphone, + Webhook, } from 'lucide-react' import React, { useEffect, useRef, useState } from 'react' import ProductClient from '../webhooks/(components)/webhook-table' @@ -54,7 +55,16 @@ const WebhooksHistory = () => { .then((res) => res.data), }) + const { data: webhooks, isLoading: isLoadingWebhooks } = useQuery({ + queryKey: ['webhooks'], + queryFn: () => + httpBrowserClient + .get(ApiEndpoints.gateway.getWebhooks()) + .then((res) => res.data), + }) + const [currentDevice, setCurrentDevice] = useState('all') + const [currentWebhook, setCurrentWebhook] = useState('all') const [eventType, setEventType] = useState('all') const [status, setStatus] = useState('all') const [dateRange, setDateRange] = useState('90') @@ -84,6 +94,7 @@ const WebhooksHistory = () => { page, limit, currentDevice, + currentWebhook, status, ], enabled: true, @@ -94,6 +105,8 @@ const WebhooksHistory = () => { dateQuery.start }&end=${dateQuery.end}&deviceId=${ currentDevice === 'all' ? '' : currentDevice + }&webhookSubscriptionId=${ + currentWebhook === 'all' ? '' : currentWebhook }` ) .then((res) => res.data), @@ -151,6 +164,11 @@ const WebhooksHistory = () => { setPage(1) } + const handleWebhookChange = (webhookId: string) => { + setCurrentWebhook(webhookId) + setPage(1) + } + const message_events = [ 'MESSAGE_RECEIVED', 'MESSAGE_SENT', @@ -197,6 +215,43 @@ const WebhooksHistory = () => { +
+
+ +

Webhook

+
+ +
+
diff --git a/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx b/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx index 01d61d9..425e2f1 100644 --- a/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx @@ -42,6 +42,10 @@ import { useToast } from '@/hooks/use-toast' import { useMutation, useQueryClient } from '@tanstack/react-query' const formSchema = z.object({ + name: z + .string() + .max(64, { message: 'Name must be 64 characters or fewer' }) + .optional(), deliveryUrl: z.string().url({ message: 'Please enter a valid URL' }), events: z.array(z.string()).min(1, { message: 'Select at least one event' }), isActive: z.boolean().default(true), @@ -63,6 +67,7 @@ export function CreateWebhookDialog({ const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { + name: '', deliveryUrl: '', events: [WEBHOOK_EVENTS.MESSAGE_RECEIVED], isActive: true, @@ -71,8 +76,16 @@ export function CreateWebhookDialog({ }) const createWebhookMutation = useMutation({ - mutationFn: (values: z.infer) => - httpBrowserClient.post(ApiEndpoints.gateway.createWebhook(), values), + mutationFn: (values: z.infer) => { + const payload = { + ...values, + name: values.name?.trim() ? values.name.trim() : undefined, + } + return httpBrowserClient.post( + ApiEndpoints.gateway.createWebhook(), + payload, + ) + }, onSuccess: () => { toast({ title: 'Success', @@ -82,10 +95,11 @@ export function CreateWebhookDialog({ onOpenChange(false) form.reset() }, - onError: () => { + onError: (error: any) => { toast({ title: 'Error', - description: 'Failed to create webhook', + description: + error?.response?.data?.message || 'Failed to create webhook', variant: 'destructive', }) }, @@ -118,6 +132,26 @@ export function CreateWebhookDialog({
+ ( + + Name (optional) + + + + + A short label to help you tell your webhooks apart + + + + )} + /> void + webhookId: string + webhookLabel?: string + onDeleted?: () => void } -export function DeleteWebhookButton({ onDelete }: DeleteWebhookButtonProps) { +export function DeleteWebhookButton({ + webhookId, + webhookLabel, + onDeleted, +}: DeleteWebhookButtonProps) { + const [open, setOpen] = useState(false) + const queryClient = useQueryClient() + const { toast } = useToast() + + const { mutate: deleteWebhook, isPending } = useMutation({ + mutationFn: () => + httpBrowserClient.delete(ApiEndpoints.gateway.deleteWebhook(webhookId)), + onSuccess: () => { + toast({ + title: 'Webhook deleted', + description: webhookLabel + ? `"${webhookLabel}" has been removed.` + : 'The webhook has been removed.', + }) + queryClient.invalidateQueries({ queryKey: ['webhooks'] }) + setOpen(false) + onDeleted?.() + }, + onError: (error: any) => { + toast({ + title: 'Error', + description: + error?.response?.data?.message || 'Failed to delete webhook', + variant: 'destructive', + }) + }, + }) + return ( - + - @@ -30,18 +69,24 @@ export function DeleteWebhookButton({ onDelete }: DeleteWebhookButtonProps) { Delete Webhook - Are you sure you want to delete this webhook? This action cannot be - undone. + {webhookLabel + ? `Are you sure you want to delete "${webhookLabel}"? ` + : 'Are you sure you want to delete this webhook? '} + New events will no longer be delivered to this endpoint. Past + delivery history will be preserved. - Cancel + Cancel { + e.preventDefault() + deleteWebhook() + }} + disabled={isPending} className='bg-destructive text-destructive-foreground hover:bg-destructive/90' > - Delete + {isPending ? 'Deleting...' : 'Delete'} diff --git a/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx b/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx index f24353c..e8ba690 100644 --- a/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx @@ -43,6 +43,10 @@ import { } from '@/components/ui/dropdown-menu' const formSchema = z.object({ + name: z + .string() + .max(64, { message: 'Name must be 64 characters or fewer' }) + .optional(), deliveryUrl: z.string().url({ message: 'Please enter a valid URL' }), events: z.array(z.string()).min(1, { message: 'Select at least one event' }), isActive: z.boolean().default(true), @@ -66,6 +70,7 @@ export function EditWebhookDialog({ const form = useForm>({ resolver: zodResolver(formSchema), values: { + name: webhook.name ?? '', deliveryUrl: webhook.deliveryUrl, events: webhook.events, isActive: webhook.isActive, @@ -75,9 +80,13 @@ export function EditWebhookDialog({ const { mutate: updateWebhook, isPending } = useMutation({ mutationFn: async (values: z.infer) => { + const payload = { + ...values, + name: values.name?.trim() ? values.name.trim() : '', + } return httpBrowserClient.patch( ApiEndpoints.gateway.updateWebhook(webhook._id), - values + payload, ) }, onSuccess: () => { @@ -89,10 +98,11 @@ export function EditWebhookDialog({ queryClient.invalidateQueries({ queryKey: ['webhooks'] }) onOpenChange(false) }, - onError: () => { + onError: (error: any) => { toast({ title: 'Error', - description: 'Failed to update webhook', + description: + error?.response?.data?.message || 'Failed to update webhook', variant: 'destructive', }) }, @@ -124,6 +134,26 @@ export function EditWebhookDialog({ + ( + + Name (optional) + + + + + A short label to help you tell your webhooks apart + + + + )} + /> void - onDelete?: () => void + onDeleted?: () => void } -export function WebhookCard({ webhook, onEdit, onDelete }: WebhookCardProps) { +export function WebhookCard({ webhook, onEdit, onDeleted }: WebhookCardProps) { const { toast } = useToast() const [isLoading, setIsLoading] = useState(false) const queryClient = useQueryClient() @@ -68,7 +68,9 @@ export function WebhookCard({ webhook, onEdit, onDelete }: WebhookCardProps) {
-

Webhook Endpoint

+

+ {webhook.name?.trim() ? webhook.name : 'Webhook Endpoint'} +

{webhook.isActive ? 'Active' : 'Inactive'} @@ -78,8 +80,8 @@ export function WebhookCard({ webhook, onEdit, onDelete }: WebhookCardProps) {

- @@ -87,7 +89,11 @@ export function WebhookCard({ webhook, onEdit, onDelete }: WebhookCardProps) { Edit - +
diff --git a/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx b/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx index 5d992f7..0d495c3 100644 --- a/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx @@ -12,13 +12,6 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' import httpBrowserClient from '@/lib/httpBrowserClient' import { ApiEndpoints } from '@/config/api' import { Skeleton } from '@/components/ui/skeleton' -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip' -import Link from 'next/link' import { useRouter } from 'next/navigation' function WebhookCardSkeleton() { @@ -56,6 +49,8 @@ function WebhookCardSkeleton() { ) } +const MAX_WEBHOOKS_PER_USER = 5 + export default function WebhooksSection() { const [createDialogOpen, setCreateDialogOpen] = useState(false) const [editDialogOpen, setEditDialogOpen] = useState(false) @@ -86,6 +81,9 @@ export default function WebhooksSection() { setEditDialogOpen(true) } + const webhookCount = webhooks?.data?.length ?? 0 + const reachedLimit = webhookCount >= MAX_WEBHOOKS_PER_USER + return (
@@ -95,35 +93,25 @@ export default function WebhooksSection() { Webhooks

- Manage webhook notifications for your SMS events + Manage webhook notifications for your SMS events. You can configure + up to {MAX_WEBHOOKS_PER_USER} webhooks.

- - - -
- -
-
- {webhooks?.data?.length > 0 && ( - -

- You already have an active webhook subscription. You can - edit or manage the existing webhook instead. -

-
- )} -
-
+