From f8646d67fc25f0d0fe28ffd5374d1beb8991b380 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Fri, 17 Apr 2026 16:54:13 -0400 Subject: [PATCH 01/90] refactor: update webhooks page structure and remove unused components - Refactored the webhooks page to utilize a new layout with improved content organization. - Replaced the previous card and frame structure with a more streamlined Page component. - Introduced WebhooksPageContent for better separation of concerns and added search functionality. - Removed unused WebhooksEmpty and TableBody components to clean up the codebase. - Updated WebhooksTable to handle error states and display appropriate messages based on webhook data. --- .../dashboard/[teamSlug]/webhooks/page.tsx | 53 ++----- .../dashboard/settings/webhooks/empty.tsx | 24 ---- .../settings/webhooks/table-body.tsx | 46 ------ .../dashboard/settings/webhooks/table.tsx | 96 ++++++++++--- .../webhooks/webhooks-page-content.tsx | 131 ++++++++++++++++++ 5 files changed, 222 insertions(+), 128 deletions(-) delete mode 100644 src/features/dashboard/settings/webhooks/empty.tsx delete mode 100644 src/features/dashboard/settings/webhooks/table-body.tsx create mode 100644 src/features/dashboard/settings/webhooks/webhooks-page-content.tsx diff --git a/src/app/dashboard/[teamSlug]/webhooks/page.tsx b/src/app/dashboard/[teamSlug]/webhooks/page.tsx index 11146c781..c882c042a 100644 --- a/src/app/dashboard/[teamSlug]/webhooks/page.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/page.tsx @@ -1,16 +1,8 @@ -import { Plus } from 'lucide-react' import { notFound } from 'next/navigation' import { INCLUDE_ARGUS } from '@/configs/flags' -import WebhookAddEditDialog from '@/features/dashboard/settings/webhooks/add-edit-dialog' -import WebhooksTable from '@/features/dashboard/settings/webhooks/table' -import Frame from '@/ui/frame' -import { Button } from '@/ui/primitives/button' -import { - Card, - CardContent, - CardDescription, - CardHeader, -} from '@/ui/primitives/card' +import { getWebhooks } from '@/core/server/functions/webhooks/get-webhooks' +import { Page } from '@/features/dashboard/layouts/page' +import { WebhooksPageContent } from '@/features/dashboard/settings/webhooks/webhooks-page-content' interface WebhooksPageClientProps { params: Promise<{ @@ -25,36 +17,17 @@ export default async function WebhooksPage({ return notFound() } - return ( - - - -
- - Webhooks allow your external service to be notified when sandbox - lifecycle events happen. When the specified event happens, we'll - send a POST request to the configured URLs. - + const { teamSlug } = await params + + const webhooksResult = await getWebhooks({ teamSlug }) - - - -
-
+ const hasError = webhooksResult?.data === undefined - -
- -
-
-
- + const webhooks = webhooksResult?.data?.webhooks ?? [] + + return ( + + + ) } diff --git a/src/features/dashboard/settings/webhooks/empty.tsx b/src/features/dashboard/settings/webhooks/empty.tsx deleted file mode 100644 index b28dbf825..000000000 --- a/src/features/dashboard/settings/webhooks/empty.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { cn } from '@/lib/utils' -import { WebhookIcon } from '@/ui/primitives/icons' - -interface WebhooksEmptyProps { - error?: string -} - -export default function WebhooksEmpty({ error }: WebhooksEmptyProps) { - return ( -
- -

- {error ? error : 'No webhooks added yet'} -

-
- ) -} diff --git a/src/features/dashboard/settings/webhooks/table-body.tsx b/src/features/dashboard/settings/webhooks/table-body.tsx deleted file mode 100644 index fa436a298..000000000 --- a/src/features/dashboard/settings/webhooks/table-body.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { getWebhooks } from '@/core/server/functions/webhooks/get-webhooks' -import { TableCell, TableRow } from '@/ui/primitives/table' -import WebhooksEmpty from './empty' -import WebhookTableRow from './table-row' - -interface TableBodyContentProps { - params: Promise<{ - teamSlug: string - }> -} - -export default async function TableBodyContent({ - params, -}: TableBodyContentProps) { - const { teamSlug } = await params - const webhooksResult = await getWebhooks({ teamSlug }) - - // undefined data indicates execution error so we disable the controls - const hasError = webhooksResult?.data === undefined - // normalize data field, no matter the execution result - const data = webhooksResult?.data ? webhooksResult.data : { webhooks: [] } - - if (hasError || !data.webhooks.length) { - return ( - - - - - - ) - } - - return ( - <> - {data.webhooks.map((webhook, index) => ( - - ))} - - ) -} diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index 4c4342413..7df06c834 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -1,37 +1,97 @@ -import { type FC, Suspense } from 'react' import { cn } from '@/lib/utils' +import { WebhookIcon } from '@/ui/primitives/icons' import { Table, TableBody, + TableEmptyState, TableHead, TableHeader, TableRow, } from '@/ui/primitives/table' -import { TableLoader } from '@/ui/table-loader' -import TableBodyContent from './table-body' +import WebhookTableRow from './table-row' +import type { Webhook } from './types' interface WebhooksTableProps { - params: Promise<{ - teamSlug: string - }> + webhooks: Webhook[] + totalWebhookCount: number + hasError: boolean + hasActiveSearch: boolean className?: string } -const WebhooksTable: FC = ({ params, className }) => { +const WebhooksTable = ({ + webhooks, + totalWebhookCount, + hasError, + hasActiveSearch, + className, +}: WebhooksTableProps) => { + const hasNoWebhooks = totalWebhookCount === 0 + const emptyMessage = hasError + ? 'Failed to get webhooks. Try again or contact support.' + : hasNoWebhooks + ? 'No webhooks added yet' + : hasActiveSearch + ? 'No webhooks match your search.' + : 'No webhooks added yet' + return ( - - - - Name & URL - Events - Added - +
+ + + + + + + + + + Name & URL + + + Events + + + Added + + + Actions + - - }> - - + + {hasError || webhooks.length === 0 ? ( + + + + {emptyMessage} + + + ) : ( + webhooks.map((webhook, index) => ( + + )) + )}
) diff --git a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx new file mode 100644 index 000000000..c34ad89c3 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx @@ -0,0 +1,131 @@ +'use client' + +import { useMemo, useState } from 'react' +import type { Webhook } from '@/features/dashboard/settings/webhooks/types' +import { cn } from '@/lib/utils' +import { pluralize } from '@/lib/utils/formatting' +import { Button } from '@/ui/primitives/button' +import { AddIcon, SearchIcon } from '@/ui/primitives/icons' +import { Input } from '@/ui/primitives/input' +import WebhookAddEditDialog from './add-edit-dialog' +import WebhooksTable from './table' + +interface WebhooksPageContentProps { + webhooks: Webhook[] + hasError: boolean + className?: string +} + +interface WebhooksSearchFieldProps { + value: string + onChange: (next: string) => void + count: number +} + +const WebhooksSearchField = ({ + value, + onChange, + count, +}: WebhooksSearchFieldProps) => { + const placeholder = + count === 0 + ? 'Add a webhook to start searching' + : 'Search by name, URL, or event' + + return ( +
+ + onChange(event.target.value)} + placeholder={placeholder} + type="search" + value={value} + /> +
+ ) +} + +interface WebhooksTotalLabelProps { + totalCount: number + filteredCount: number + hasActiveSearch: boolean +} + +const WebhooksTotalLabel = ({ + totalCount, + filteredCount, + hasActiveSearch, +}: WebhooksTotalLabelProps) => { + if (totalCount === 0) return null + + const label = hasActiveSearch + ? `Showing ${filteredCount} of ${totalCount} ${pluralize(totalCount, 'webhook')}` + : `${totalCount} ${pluralize(totalCount, 'webhook')} in total` + + return

{label}

+} + +export const WebhooksPageContent = ({ + webhooks, + hasError, + className, +}: WebhooksPageContentProps) => { + const [query, setQuery] = useState('') + const normalizedQuery = query.trim().toLowerCase() + const hasActiveSearch = normalizedQuery.length > 0 + + const filteredWebhooks = useMemo(() => { + if (!normalizedQuery) return webhooks + + return webhooks.filter(({ events, name, url }) => { + return [name, url, ...events].some((value) => + value.toLowerCase().includes(normalizedQuery) + ) + }) + }, [normalizedQuery, webhooks]) + + return ( +
+
+ + + + + +
+ +
+

+ Receive POST requests to your URLs when sandbox lifecycle events + occur. +

+ +
+ +
+ +
+
+ ) +} From 2e0e1da227bd49f98792188fec7e9ba4bad70132 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Fri, 17 Apr 2026 17:22:27 -0400 Subject: [PATCH 02/90] refactor: simplify webhooks table and update styling - Removed the unused `hasActiveSearch` prop from the `WebhooksTable` component. - Streamlined the logic for displaying empty states based on webhook data. - Updated the `WebhooksPageContent` component to enhance layout and button styling. - Added `.cursor/` to `.gitignore` to exclude cursor-related files from version control. --- .gitignore | 1 + .../dashboard/settings/webhooks/table.tsx | 39 ++++++++----------- .../webhooks/webhooks-page-content.tsx | 12 ++++-- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index b6b85df6d..26a5f848a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ next-env.d.ts # AI agents and related files CLAUDE.md +.cursor/ .agent diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index 7df06c834..e55f242ae 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -15,7 +15,6 @@ interface WebhooksTableProps { webhooks: Webhook[] totalWebhookCount: number hasError: boolean - hasActiveSearch: boolean className?: string } @@ -23,17 +22,12 @@ const WebhooksTable = ({ webhooks, totalWebhookCount, hasError, - hasActiveSearch, className, }: WebhooksTableProps) => { const hasNoWebhooks = totalWebhookCount === 0 - const emptyMessage = hasError - ? 'Failed to get webhooks. Try again or contact support.' - : hasNoWebhooks - ? 'No webhooks added yet' - : hasActiveSearch - ? 'No webhooks match your search.' - : 'No webhooks added yet' + const emptyMessage = hasNoWebhooks + ? 'No webhooks added yet' + : 'No webhooks match your search.' return ( @@ -60,27 +54,26 @@ const WebhooksTable = ({ - {hasError || webhooks.length === 0 ? ( + {hasError ? ( + + +

+ Failed to get webhooks. Try again or contact support. +

+
+ ) : webhooks.length === 0 ? ( - - {emptyMessage} - +

{emptyMessage}

) : ( webhooks.map((webhook, index) => ( diff --git a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx index c34ad89c3..3ec0e39f2 100644 --- a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx +++ b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx @@ -90,7 +90,7 @@ export const WebhooksPageContent = ({ }, [normalizedQuery, webhooks]) return ( -
+
- @@ -120,7 +125,6 @@ export const WebhooksPageContent = ({
Date: Thu, 23 Apr 2026 13:48:07 -0400 Subject: [PATCH 03/90] Fix type errors --- src/__test__/unit/sandbox-monitoring-chart-model.test.ts | 1 + .../dashboard/settings/webhooks/webhooks-page-content.tsx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__test__/unit/sandbox-monitoring-chart-model.test.ts b/src/__test__/unit/sandbox-monitoring-chart-model.test.ts index 848a982ef..adf1e9819 100644 --- a/src/__test__/unit/sandbox-monitoring-chart-model.test.ts +++ b/src/__test__/unit/sandbox-monitoring-chart-model.test.ts @@ -9,6 +9,7 @@ const baseMetric = { timestamp: '1970-01-01T00:00:00.000Z', cpuCount: 2, memTotal: 1_000, + memCache: 0, diskTotal: 2_000, } satisfies Omit< SandboxMetric, diff --git a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx index 3ec0e39f2..fd0276458 100644 --- a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx +++ b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx @@ -101,9 +101,8 @@ export const WebhooksPageContent = ({ + )} +
+ ) } ) Input.displayName = 'Input' From 50b1a30786c1829d90e2da5edd9a79737879f972 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Fri, 24 Apr 2026 11:18:47 -0400 Subject: [PATCH 07/90] refactor: enhance webhook dialog with dynamic example payload - Updated `WebhookExamplePayload` to accept an `eventType` prop for dynamic rendering of example payloads. - Simplified the display of the webhook secret description and removed the link to signature validation documentation. - Introduced state management for the last selected event to improve user experience in the webhook dialog. - Adjusted layout and styling for better clarity and usability in the webhook add/edit dialog. --- .../webhooks/add-edit-dialog-steps.tsx | 37 ++++------ .../settings/webhooks/add-edit-dialog.tsx | 70 ++++++++++--------- 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx index 3fd10aac2..14def08f4 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx @@ -13,7 +13,7 @@ import { FormLabel, FormMessage, } from '@/ui/primitives/form' -import { CopyIcon, ExternalLinkIcon, WarningIcon } from '@/ui/primitives/icons' +import { CopyIcon, WarningIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' @@ -22,7 +22,7 @@ import { WEBHOOK_DOCS_URL, WEBHOOK_EVENT_LABELS, WEBHOOK_EVENTS, - WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL, + type WebhookEvent, } from './constants' type WebhookAddEditDialogStepsProps = { @@ -30,20 +30,21 @@ type WebhookAddEditDialogStepsProps = { form: UseFormReturn isLoading: boolean selectedEvents: string[] + exampleEventType: WebhookEvent allEventsSelected: boolean handleAllToggle: () => void handleEventToggle: (event: string) => void mode: 'add' | 'edit' } -const WebhookExamplePayload = () => ( +const WebhookExamplePayload = ({ eventType }: { eventType: WebhookEvent }) => (
{'{'}
{' '} {'"type"'} - {': "sandbox.lifecycle.created",'} + {`: "${eventType}",`}
{' '} @@ -76,6 +77,7 @@ export function WebhookAddEditDialogSteps({ form, isLoading, selectedEvents, + exampleEventType, allEventsSelected, handleAllToggle, handleEventToggle, @@ -237,14 +239,14 @@ export function WebhookAddEditDialogSteps({ {/* Description */}
-

+

We'll send a POST request with a JSON payload to{' '} {form.watch('url') || 'https://example.com/postreceive'} {' '} for each event. Example:

- +
)} @@ -260,24 +262,11 @@ export function WebhookAddEditDialogSteps({ > {/* Section Title and Description */}
-

- Signature Secret -

+

Secret

- This secret is used to verify webhook authenticity. Each request - includes an e2b-signature header - generated with HMAC SHA-256. Validate this in your endpoint to - ensure requests are from E2B and untampered. + A secret verifies that webhooks are from us and untampered. Use + our pre-generated one or add your own.

- - View validation examples - -
{/* Tabs */} @@ -332,8 +321,8 @@ export function WebhookAddEditDialogSteps({

- Store this secret securely. You won't be able to view it - again after creating the webhook. + Copy and store it now. You won't be able to view it + again.

diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index 37aa2681f..bc75ba03e 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -24,7 +24,7 @@ import { AddIcon, CheckIcon } from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader' import { useDashboard } from '../../context' import { WebhookAddEditDialogSteps } from './add-edit-dialog-steps' -import { WEBHOOK_EVENTS } from './constants' +import { WEBHOOK_EVENTS, type WebhookEvent } from './constants' import type { Webhook } from './types' type WebhookAddEditDialogProps = @@ -49,6 +49,8 @@ export default function WebhookAddEditDialog({ const { team } = useDashboard() const [open, setOpen] = useState(false) const [currentStep, setCurrentStep] = useState(1) + const [lastSelectedEvent, setLastSelectedEvent] = + useState(null) const isEditMode = mode === 'edit' const totalSteps = isEditMode ? 1 : 2 @@ -156,9 +158,17 @@ export default function WebhookAddEditDialog({ ) } else { form.setValue('events', [...currentEvents, event]) + const matched = WEBHOOK_EVENTS.find((e) => e === event) + if (matched) setLastSelectedEvent(matched) } } + const exampleEventType: WebhookEvent = + lastSelectedEvent && selectedEvents.includes(lastSelectedEvent) + ? lastSelectedEvent + : (WEBHOOK_EVENTS.find((event) => selectedEvents.includes(event)) ?? + WEBHOOK_EVENTS[0]) + const handleNext = async () => { if (currentStep === 1) { const isNameValid = await form.trigger('name') @@ -205,6 +215,7 @@ export default function WebhookAddEditDialog({ form={form} isLoading={isLoading} selectedEvents={selectedEvents} + exampleEventType={exampleEventType} allEventsSelected={allEventsSelected} handleAllToggle={handleAllToggle} handleEventToggle={handleEventToggle} @@ -232,39 +243,34 @@ export default function WebhookAddEditDialog({ Confirm + ) : currentStep === 1 ? ( + ) : ( - /* Add mode: show next/back navigation */ <> - {currentStep === 1 ? ( - - ) : ( - <> - - - - )} + + )} From 333ad34164240722c18e3e187055fc0a34360b10 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Fri, 24 Apr 2026 13:57:09 -0400 Subject: [PATCH 08/90] refactor: enhance webhook add/edit dialog with improved focus and styling - Added a custom input ref to focus on the secret input when 'custom' secret type is selected. - Updated the button to show a check icon when the copy action is successful, improving user feedback. - Adjusted layout and styling for better responsiveness and clarity in the dialog components. --- .../webhooks/add-edit-dialog-steps.tsx | 34 +++++++++++++++---- .../settings/webhooks/add-edit-dialog.tsx | 13 ++++--- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx index 14def08f4..ee4ad3f56 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx @@ -1,7 +1,7 @@ 'use client' import { AnimatePresence, motion } from 'motion/react' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import type { UseFormReturn } from 'react-hook-form' import type { UpsertWebhookSchemaType } from '@/core/server/functions/webhooks/schema' import { Button } from '@/ui/primitives/button' @@ -13,7 +13,7 @@ import { FormLabel, FormMessage, } from '@/ui/primitives/form' -import { CopyIcon, WarningIcon } from '@/ui/primitives/icons' +import { CheckIcon, CopyIcon, WarningIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' @@ -97,6 +97,15 @@ export function WebhookAddEditDialogSteps({ const [copied, setCopied] = useState(false) + const customSecretInputRef = useRef(null) + useEffect(() => { + if (secretType !== 'custom') return + const id = window.setTimeout(() => { + customSecretInputRef.current?.focus() + }, 0) + return () => window.clearTimeout(id) + }, [secretType]) + // sync secret with form state and validation - only in 'add' mode // in 'edit' mode, we should never touch the signature secret useEffect(() => { @@ -312,14 +321,21 @@ export function WebhookAddEditDialogSteps({ type="button" onClick={handleCopy} disabled={isLoading} - className="shrink-0" + className="shrink-0 relative" > - - {copied ? 'Copied' : 'Copy'} + + + Copy + + {copied && ( + + )}
-
- +
+

Copy and store it now. You won't be able to view it again. @@ -343,6 +359,10 @@ export function WebhookAddEditDialogSteps({ disabled={isLoading} className="min-w-0" {...field} + ref={(el) => { + field.ref(el) + customSecretInputRef.current = el + }} />

diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index bc75ba03e..441ffc2c1 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -188,8 +188,8 @@ export default function WebhookAddEditDialog({

{trigger} - - + + {isEditMode ? 'Edit Webhook' : 'Add Webhook'} @@ -204,12 +204,15 @@ export default function WebhookAddEditDialog({
- + {/* Hidden fields */} -
+
@@ -268,6 +272,7 @@ export default function WebhookAddEditDialog({ + ) : currentStep === 1 ? ( + ) : ( <> - {/* Edit mode: show submit button directly */} - {isEditMode ? ( - - ) : currentStep === 1 ? ( - - ) : ( - <> - - - - )} + + )} diff --git a/src/features/dashboard/settings/webhooks/delete-dialog.tsx b/src/features/dashboard/settings/webhooks/delete-dialog.tsx index a6a0049a2..55bdc66c4 100644 --- a/src/features/dashboard/settings/webhooks/delete-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/delete-dialog.tsx @@ -1,14 +1,14 @@ 'use client' -import { useAction } from 'next-safe-action/hooks' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' -import { deleteWebhookAction } from '@/core/server/actions/webhooks-actions' import { useDashboard } from '@/features/dashboard/context' import { defaultErrorToast, defaultSuccessToast, useToast, } from '@/lib/hooks/use-toast' +import { useTRPC } from '@/trpc/client' import { AlertDialog } from '@/ui/alert-dialog' import { TrashIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' @@ -27,27 +27,33 @@ export default function WebhookDeleteDialog({ }: WebhookDeleteDialogProps) { const { team } = useDashboard() const { toast } = useToast() + const trpc = useTRPC() + const queryClient = useQueryClient() const [open, setOpen] = useState(false) const [confirmationUrl, setConfirmationUrl] = useState('') const isUrlMatch = confirmationUrl === webhook.url - const { execute: executeDeleteWebhook, isExecuting: isDeleting } = useAction( - deleteWebhookAction, - { + const listQueryKey = trpc.webhooks.list.queryOptions({ + teamSlug: team.slug, + }).queryKey + + const deleteMutation = useMutation( + trpc.webhooks.delete.mutationOptions({ onSuccess: () => { toast(defaultSuccessToast('Webhook deleted successfully')) + void queryClient.invalidateQueries({ queryKey: listQueryKey }) setOpen(false) setConfirmationUrl('') }, - onError: ({ error }) => { - toast( - defaultErrorToast(error.serverError || 'Failed to delete webhook') - ) + onError: (err) => { + toast(defaultErrorToast(err.message || 'Failed to delete webhook')) }, - } + }) ) + const isDeleting = deleteMutation.isPending + const handleOpenChange = (value: boolean) => { setOpen(value) if (!value) { @@ -82,7 +88,7 @@ export default function WebhookDeleteDialog({ disabled: isDeleting || !isUrlMatch, }} onConfirm={() => { - executeDeleteWebhook({ + deleteMutation.mutate({ teamSlug: team.slug, webhookId: webhook.id, }) diff --git a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx b/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx index 6e86312cc..00bd21f50 100644 --- a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx @@ -1,16 +1,20 @@ 'use client' import { zodResolver } from '@hookform/resolvers/zod' -import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' -import { updateWebhookSecretAction } from '@/core/server/actions/webhooks-actions' -import { UpdateWebhookSecretSchema } from '@/core/server/functions/webhooks/schema' +import { useForm } from 'react-hook-form' +import { + type UpdateWebhookSecretInput, + UpdateWebhookSecretInputSchema, +} from '@/core/server/functions/webhooks/schema' import { useDashboard } from '@/features/dashboard/context' import { defaultErrorToast, defaultSuccessToast, toast, } from '@/lib/hooks/use-toast' +import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' import { Dialog, @@ -44,55 +48,60 @@ export default function WebhookEditSecretDialog({ 'use no memo' const { team } = useDashboard() + const trpc = useTRPC() + const queryClient = useQueryClient() const [open, setOpen] = useState(false) const webhookName = webhook.name - const { - form, - resetFormAndAction, - handleSubmitWithAction, - action: { isPending: isLoading }, - } = useHookFormAction( - updateWebhookSecretAction, - zodResolver(UpdateWebhookSecretSchema), - { - formProps: { - mode: 'onChange', - defaultValues: { - teamSlug: team.slug, - webhookId: webhook.id, - signatureSecret: '', - }, + const listQueryKey = trpc.webhooks.list.queryOptions({ + teamSlug: team.slug, + }).queryKey + + const form = useForm({ + resolver: zodResolver(UpdateWebhookSecretInputSchema), + mode: 'onChange', + defaultValues: { + webhookId: webhook.id, + signatureSecret: '', + }, + }) + + const updateSecretMutation = useMutation( + trpc.webhooks.updateSecret.mutationOptions({ + onSuccess: () => { + toast(defaultSuccessToast('Webhook secret rotated successfully')) + void queryClient.invalidateQueries({ queryKey: listQueryKey }) + handleDialogChange(false) }, - actionProps: { - onSuccess: () => { - toast(defaultSuccessToast('Webhook secret rotated successfully')) - handleDialogChange(false) - }, - onError: ({ error }) => { - toast( - defaultErrorToast( - error.serverError || 'Failed to rotate webhook secret' - ) - ) - }, + onError: (err) => { + toast( + defaultErrorToast(err.message || 'Failed to rotate webhook secret') + ) }, - } + }) ) + const isLoading = updateSecretMutation.isPending + const handleDialogChange = (value: boolean) => { setOpen(value) if (value) return - resetFormAndAction() + form.reset() + updateSecretMutation.reset() } - // watch field to trigger reactive updates + const handleSubmit = form.handleSubmit((values) => { + updateSecretMutation.mutate({ + ...values, + teamSlug: team.slug, + }) + }) + const signatureSecret = form.watch('signatureSecret') - // use form state for validation - sync with zod schema const { errors } = form.formState const isSecretValid = !errors.signatureSecret && signatureSecret && signatureSecret.length >= 32 @@ -135,9 +144,7 @@ export default function WebhookEditSecretDialog({ - - {/* Hidden fields */} - +
diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index d8fdbe699..87d778708 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -14,14 +14,12 @@ import type { Webhook } from './types' interface WebhooksTableProps { webhooks: Webhook[] totalWebhookCount: number - hasError: boolean className?: string } const WebhooksTable = ({ webhooks, totalWebhookCount, - hasError, className, }: WebhooksTableProps) => { const hasNoWebhooks = totalWebhookCount === 0 @@ -59,17 +57,7 @@ const WebhooksTable = ({ '[&_tr:last-child]:border-b [&_tr:last-child]:border-stroke/80' )} > - {hasError ? ( - - -

- Failed to get webhooks. Try again or contact support. -

-
- ) : webhooks.length === 0 ? ( + {webhooks.length === 0 ? ( { + const trpc = useTRPC() const [query, setQuery] = useState('') const normalizedQuery = query.trim().toLowerCase() const hasActiveSearch = normalizedQuery.length > 0 + const { data } = useSuspenseQuery( + trpc.webhooks.list.queryOptions({ teamSlug }) + ) + + const webhooks = data.webhooks + const filteredWebhooks = useMemo(() => { if (!normalizedQuery) return webhooks @@ -124,7 +130,6 @@ export const WebhooksPageContent = ({
From 59aa65dcc78749db71cb60ffe41b1af818975526 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 4 May 2026 13:31:42 -0400 Subject: [PATCH 12/90] feat: add maxLength to custom secret input in webhook dialog --- .../dashboard/settings/webhooks/add-edit-dialog-steps.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx index 2ac3019da..c6126758e 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx @@ -357,6 +357,7 @@ export function WebhookAddEditDialogSteps({ { From 031df161799931cfb281214d04674c089ae95e97 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 4 May 2026 13:57:29 -0400 Subject: [PATCH 13/90] feat: integrate FinishWebhookSetupDialog for enhanced webhook setup experience - Added a new FinishWebhookSetupDialog component to guide users through verifying webhook signatures. - Implemented language selection for verification snippets (JavaScript and Python) to assist users in integrating webhook security. - Updated the existing WebhookAddEditDialog to trigger the new dialog upon successful webhook addition, improving user flow. This enhancement aims to streamline the webhook setup process and provide clear guidance on signature verification. --- .../settings/webhooks/add-edit-dialog.tsx | 177 ++++++++++-------- .../webhooks/finish-webhook-setup-dialog.tsx | 157 ++++++++++++++++ 2 files changed, 253 insertions(+), 81 deletions(-) create mode 100644 src/features/dashboard/settings/webhooks/finish-webhook-setup-dialog.tsx diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index 245353a0e..65fa50e1d 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -29,6 +29,7 @@ import { Loader } from '@/ui/primitives/loader' import { useDashboard } from '../../context' import { WebhookAddEditDialogSteps } from './add-edit-dialog-steps' import { WEBHOOK_EVENTS, type WebhookEvent } from './constants' +import { FinishWebhookSetupDialog } from './finish-webhook-setup-dialog' import type { Webhook } from './types' type WebhookAddEditDialogProps = @@ -58,6 +59,7 @@ export default function WebhookAddEditDialog({ const [lastSelectedEvent, setLastSelectedEvent] = useState(null) const [hasCopied, setHasCopied] = useState(false) + const [finishSetupDialogOpen, setFinishSetupDialogOpen] = useState(false) const isEditMode = mode === 'edit' const totalSteps = isEditMode ? 1 : 2 @@ -94,6 +96,13 @@ export default function WebhookAddEditDialog({ ) ) void queryClient.invalidateQueries({ queryKey: listQueryKey }) + + if (!isEditMode) { + handleDialogChange(false) + setFinishSetupDialogOpen(true) + return + } + handleDialogChange(false) }, onError: (err) => { @@ -195,98 +204,104 @@ export default function WebhookAddEditDialog({ } return ( - - {trigger} + <> + + {trigger} - - - - {isEditMode ? 'Edit Webhook' : 'Add Webhook'} - - {!isEditMode && ( -
- - Step {currentStep} / {totalSteps} - -
- )} -
+ + + + {isEditMode ? 'Edit Webhook' : 'Add Webhook'} + + {!isEditMode && ( +
+ + Step {currentStep} / {totalSteps} + +
+ )} +
- - - + + + -
- setHasCopied(true)} - /> -
+
+ setHasCopied(true)} + /> +
- - {isLoading ? ( -
- - - {isEditMode ? 'Saving Changes...' : 'Adding Webhook...'} - -
- ) : isEditMode ? ( - - ) : currentStep === 1 ? ( - - ) : ( - <> + + {isLoading ? ( +
+ + + {isEditMode ? 'Saving Changes...' : 'Adding Webhook...'} + +
+ ) : isEditMode ? ( + ) : currentStep === 1 ? ( - - )} -
- - -
-
+ ) : ( + <> + + + + )} + + + + +
+ + ) } diff --git a/src/features/dashboard/settings/webhooks/finish-webhook-setup-dialog.tsx b/src/features/dashboard/settings/webhooks/finish-webhook-setup-dialog.tsx new file mode 100644 index 000000000..f2ef2ed11 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/finish-webhook-setup-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import { useState } from 'react' +import { CodeBlock } from '@/ui/code-block' +import { Button } from '@/ui/primitives/button' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/ui/primitives/dialog' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' +import { WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL } from './constants' + +const WEBHOOK_VERIFICATION_SNIPPETS = { + javascript: `import crypto from 'crypto' + +function verifyWebhookSignature(secret : string, payload : string, payloadSignature : string) { + const expectedSignatureRaw = crypto.createHash('sha256').update(secret + payload).digest('base64'); + const expectedSignature = expectedSignatureRaw.replace(/=+$/, ''); + return expectedSignature == payloadSignature +} + +const payloadValid = verifyWebhookSignature(secret, webhookBodyRaw, webhookSignatureHeader) +if (payloadValid) { + console.log("Payload signature is valid") +} else { + console.log("Payload signature is INVALID") +}`, + python: `import hashlib +import base64 + +def verify_webhook_signature(secret: str, payload: str, payload_signature: str) -> bool: + hash_bytes = hashlib.sha256((secret + payload).encode('utf-8')).digest() + expected_signature = base64.b64encode(hash_bytes).decode('utf-8') + expected_signature = expected_signature.rstrip('=') + + return expected_signature == payload_signature + +if verify_webhook_signature(secret, webhook_body_raw, webhook_signature_header): + print("Payload signature is valid") +else: + print("Payload signature is INVALID")`, +} + +type WebhookVerificationLanguage = keyof typeof WEBHOOK_VERIFICATION_SNIPPETS + +const WEBHOOK_VERIFICATION_LANGUAGE_LABELS: Record< + WebhookVerificationLanguage, + string +> = { + javascript: 'JavaScript', + python: 'Python', +} + +// Checks if a tab value is a snippet language; "python" -> true, "go" -> false. +const isWebhookVerificationLanguage = ( + value: string +): value is WebhookVerificationLanguage => + value === 'javascript' || value === 'python' + +type FinishWebhookSetupDialogProps = { + open: boolean + onOpenChange: (open: boolean) => void +} + +export const FinishWebhookSetupDialog = ({ + open, + onOpenChange, +}: FinishWebhookSetupDialogProps) => { + const [language, setLanguage] = + useState('javascript') + + const handleLanguageChange = (value: string) => { + if (isWebhookVerificationLanguage(value)) setLanguage(value) + } + + return ( + + + + Finish Webhook Setup + + +
+
+

+ Secret Use +

+

+ Use the snippet below to verify the secret when your webhook is + called.{' '} + + Read more in the docs. + +

+
+ + + + {Object.entries(WEBHOOK_VERIFICATION_LANGUAGE_LABELS).map( + ([value, label]) => ( + + + {value === 'javascript' ? 'JS' : 'PY'} + + {label} + + ) + )} + + + {Object.entries(WEBHOOK_VERIFICATION_SNIPPETS).map( + ([value, snippet]) => ( + + + {snippet} + + + ) + )} + +
+ + + + +
+
+ ) +} From a6eaccfa9b6dfe5c57ecbeb671a823b8236a7910 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 4 May 2026 14:39:18 -0400 Subject: [PATCH 14/90] refactor: enhance WebhookTableRow component with improved structure and functionality - Refactored WebhookTableRow to separate row actions into a new WebhookRowActions component for better readability and maintainability. - Introduced a utility function to map webhook events to their corresponding labels, enhancing clarity in the UI. - Updated the table layout and styling for improved responsiveness and user experience. - Removed unnecessary props and streamlined the component's interface. This refactor aims to improve the overall structure and usability of the webhook management interface. --- .../dashboard/settings/webhooks/table-row.tsx | 165 +++++++++--------- .../dashboard/settings/webhooks/table.tsx | 19 +- 2 files changed, 94 insertions(+), 90 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 839599747..76c271511 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,6 +1,7 @@ 'use client' import { useState } from 'react' +import { cn } from '@/lib/utils' import { Badge } from '@/ui/primitives/badge' import { DropdownMenu, @@ -18,25 +19,72 @@ import { WebhookIcon, } from '@/ui/primitives/icons' import { TableCell, TableRow } from '@/ui/primitives/table' +import { useDashboard } from '../../context' +import { TeamAvatar } from '../../sidebar/team-avatar' import WebhookAddEditDialog from './add-edit-dialog' +import { WEBHOOK_EVENT_LABELS, WEBHOOK_EVENTS } from './constants' import WebhookDeleteDialog from './delete-dialog' import WebhookEditSecretDialog from './edit-secret-dialog' import type { Webhook } from './types' -interface WebhookTableRowProps { +type WebhookRowProps = { webhook: Webhook - index: number className?: string } -export default function WebhookTableRow({ - webhook, - index, - className, -}: WebhookTableRowProps) { - const [hoveredRowIndex, setHoveredRowIndex] = useState(-1) +type WebhookRowActionsProps = { + webhook: Webhook +} + +const getWebhookEventLabel = (event: string): string => { + const matchedEvent = WEBHOOK_EVENTS.find( + (webhookEvent) => webhookEvent === event + ) + if (!matchedEvent) return event + return WEBHOOK_EVENT_LABELS[matchedEvent] +} + +const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { const [dropDownOpen, setDropDownOpen] = useState(false) + return ( + + + + + + + + + + e.preventDefault()}> + Edit + + + + e.preventDefault()}> + Rotate Secret + + + + e.preventDefault()} + > + + Delete + + + + + + ) +} + +export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => { + const { team } = useDashboard() + const createdAt = webhook.createdAt ? new Date(webhook.createdAt).toLocaleDateString('en-US', { month: 'short', @@ -46,90 +94,51 @@ export default function WebhookTableRow({ : '-' return ( - setHoveredRowIndex(index)} - onMouseLeave={() => setHoveredRowIndex(-1)} - className={className} - > - {/* Name & URL Column */} - -
- {/* Icon Container */} -
+ + +
+
- {/* Name & URL */} -
-
+
+

{webhook.name} -

-
+

+

{webhook.url} -

+

- {/* Events Column */} - -
-
- {webhook.events.map((event) => ( - - {event} - - ))} -
- {/* Fade out gradient overlay */} -
+ +
+ {webhook.events.map((event) => ( + + {getWebhookEventLabel(event)} + + ))}
- {/* Added Column */} - - {createdAt} + +
+

+ {createdAt} +

+ +
- {/* Actions Column */} - - - - - - - - - - - e.preventDefault()}> - Edit - - - - e.preventDefault()}> - Rotate - Secret - - - - e.preventDefault()} - > - - Delete - - - - - + + ) diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index 87d778708..540e920fa 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -8,7 +8,7 @@ import { TableHeader, TableRow, } from '@/ui/primitives/table' -import WebhookTableRow from './table-row' +import { WebhookTableRow } from './table-row' import type { Webhook } from './types' interface WebhooksTableProps { @@ -30,10 +30,10 @@ const WebhooksTable = ({ return (
- - - - + + + + @@ -69,13 +69,8 @@ const WebhooksTable = ({

{emptyMessage}

) : ( - webhooks.map((webhook, index) => ( - + webhooks.map((webhook) => ( + )) )} From 8f32bf25557655093df85ee32b05817b3f19e384 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 4 May 2026 14:43:00 -0400 Subject: [PATCH 15/90] refactor: improve styling and structure of WebhooksTable and WebhookRowActions components - Introduced consistent class names for table row and cell styling to enhance readability and maintainability. - Updated the WebhookRowActions component to use the new class names for action icons, improving visual consistency. - Adjusted the WebhooksTable header styling for better alignment and presentation of table data. These changes aim to streamline the UI and improve the overall user experience in the webhook management interface. --- .../dashboard/settings/webhooks/table-row.tsx | 17 +++++++++++------ .../dashboard/settings/webhooks/table.tsx | 19 ++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 76c271511..5eca54d55 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -36,6 +36,9 @@ type WebhookRowActionsProps = { webhook: Webhook } +const rowCellClassName = 'h-11 p-0 align-middle' +const actionIconClassName = 'size-4 text-fg-tertiary' + const getWebhookEventLabel = (event: string): string => { const matchedEvent = WEBHOOK_EVENTS.find( (webhookEvent) => webhookEvent === event @@ -58,12 +61,12 @@ const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { e.preventDefault()}> - Edit + Edit e.preventDefault()}> - Rotate Secret + Rotate Secret @@ -95,7 +98,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => { return ( - +
@@ -112,7 +115,9 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => {
- +
{webhook.events.map((event) => ( @@ -122,7 +127,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => {
- +

{createdAt} @@ -137,7 +142,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => {

- + diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index 540e920fa..c90179b6b 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -17,6 +17,9 @@ interface WebhooksTableProps { className?: string } +const headerCellClassName = + 'h-[17px] p-0 pb-2 align-top font-sans! text-[12px] leading-[17px] font-normal text-fg-tertiary uppercase' + const WebhooksTable = ({ webhooks, totalWebhookCount, @@ -36,17 +39,15 @@ const WebhooksTable = ({
- - - Name & URL - - - Events + + NAME & URL + + EVENTS - - Added + + ADDED - + Actions From 92c521a00372a6e319dd79aa149bd1397fe0c72d Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 4 May 2026 15:11:34 -0400 Subject: [PATCH 16/90] refactor: update styling and structure of WebhookTableRow and WebhooksTable components - Adjusted class names for consistent styling across WebhookTableRow and WebhooksTable, enhancing layout and alignment. - Improved the structure of the WebhookRowActions component for better visual consistency. - Refined header cell styling in WebhooksTable for improved readability and presentation. These changes aim to enhance the user interface and overall experience in the webhook management section. --- .../dashboard/settings/webhooks/table-row.tsx | 19 ++++++++++++------- .../dashboard/settings/webhooks/table.tsx | 12 ++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 5eca54d55..8d4195ab1 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -36,7 +36,8 @@ type WebhookRowActionsProps = { webhook: Webhook } -const rowCellClassName = 'h-11 p-0 align-middle' +const rowCellClassName = 'h-[50px] p-0 align-top' +const rowContentClassName = 'flex h-11 items-center' const actionIconClassName = 'size-4 text-fg-tertiary' const getWebhookEventLabel = (event: string): string => { @@ -54,7 +55,7 @@ const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { - + @@ -97,9 +98,9 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => { : '-' return ( - + -
+
@@ -118,7 +119,9 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => { -
+
{webhook.events.map((event) => ( {getWebhookEventLabel(event)} @@ -128,7 +131,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => { -
+

{createdAt}

@@ -143,7 +146,9 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => { - +
+ +
) diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index c90179b6b..9b7aafa05 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -18,7 +18,7 @@ interface WebhooksTableProps { } const headerCellClassName = - 'h-[17px] p-0 pb-2 align-top font-sans! text-[12px] leading-[17px] font-normal text-fg-tertiary uppercase' + 'h-[17px] p-0 pb-2 align-top font-sans! text-[12px] leading-[17px] text-left font-normal text-fg-tertiary uppercase' const WebhooksTable = ({ webhooks, @@ -39,14 +39,10 @@ const WebhooksTable = ({
- + NAME & URL - - EVENTS - - - ADDED - + EVENTS + ADDED Actions From 30a5425ce6a02fcdc0364cf4d610e6ceee6aeb35 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 6 May 2026 12:47:41 -0400 Subject: [PATCH 17/90] refactor: update webhook management to use 'create' and 'update' modes - Changed the webhook input mode from 'edit' to 'update' for consistency across the application. - Refactored related components and schemas to align with the new mode terminology. - Introduced a new UpsertWebhookDialog component to streamline the webhook creation and update process. - Updated the webhooks page to utilize the new dialog, enhancing the user experience in managing webhooks. These changes aim to improve clarity and maintainability in the webhook management interface. --- .../modules/webhooks/repository.server.ts | 4 +-- src/core/server/api/routers/webhooks.ts | 6 ++-- src/core/server/functions/webhooks/schema.ts | 4 +-- .../dashboard/settings/webhooks/table-row.tsx | 6 ++-- ...ps.tsx => upsert-webhook-dialog-steps.tsx} | 14 ++++---- ...t-dialog.tsx => upsert-webhook-dialog.tsx} | 36 +++++++++---------- .../webhooks/webhooks-page-content.tsx | 6 ++-- 7 files changed, 38 insertions(+), 38 deletions(-) rename src/features/dashboard/settings/webhooks/{add-edit-dialog-steps.tsx => upsert-webhook-dialog-steps.tsx} (97%) rename src/features/dashboard/settings/webhooks/{add-edit-dialog.tsx => upsert-webhook-dialog.tsx} (91%) diff --git a/src/core/modules/webhooks/repository.server.ts b/src/core/modules/webhooks/repository.server.ts index f7188d7df..b8860f410 100644 --- a/src/core/modules/webhooks/repository.server.ts +++ b/src/core/modules/webhooks/repository.server.ts @@ -15,7 +15,7 @@ type WebhooksRepositoryDeps = { export type WebhooksScope = TeamRequestScope export interface UpsertWebhookInput { - mode: 'create' | 'edit' + mode: 'create' | 'update' webhookId?: string name: string url: string @@ -69,7 +69,7 @@ export function createWebhooksRepository( }, async upsertWebhook(input) { const response = - input.mode === 'edit' + input.mode === 'update' ? await deps.infraClient.PATCH('/events/webhooks/{webhookID}', { headers: { ...deps.authHeaders(scope.accessToken, scope.teamId), diff --git a/src/core/server/api/routers/webhooks.ts b/src/core/server/api/routers/webhooks.ts index d7f2a6c5d..fea235e3a 100644 --- a/src/core/server/api/routers/webhooks.ts +++ b/src/core/server/api/routers/webhooks.ts @@ -46,7 +46,7 @@ export const webhooksRouter = createTRPCRouter({ input const result = await ctx.webhooksRepository.upsertWebhook({ - mode: mode === 'add' ? 'create' : 'edit', + mode, webhookId: webhookId ?? undefined, name, url, @@ -59,7 +59,7 @@ export const webhooksRouter = createTRPCRouter({ l.error( { key: - mode === 'edit' + mode === 'update' ? 'update_webhook_trpc:error' : 'create_webhook_trpc:error', status: result.error.status, @@ -68,7 +68,7 @@ export const webhooksRouter = createTRPCRouter({ user_id: ctx.session.user.id, context: { mode, name, url, events }, }, - `Failed to ${mode === 'edit' ? 'update' : 'create'} webhook: ${result.error.status}: ${result.error.message}` + `Failed to ${mode} webhook: ${result.error.status}: ${result.error.message}` ) throwTRPCErrorFromRepoError(result.error) diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index f9d7af70e..19207badc 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -8,7 +8,7 @@ const WebhookSecretSchema = z export const UpsertWebhookInputSchema = z .object({ - mode: z.enum(['add', 'edit']), + mode: z.enum(['create', 'update']), webhookId: z.uuid().optional(), name: z.string().min(1, 'Name is required').trim(), url: WebhookUrlSchema, @@ -18,7 +18,7 @@ export const UpsertWebhookInputSchema = z }) .refine( (data) => { - if (data.mode === 'add') { + if (data.mode === 'create') { return !!data.signatureSecret } return true diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 8d4195ab1..ad5ae46c3 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -21,11 +21,11 @@ import { import { TableCell, TableRow } from '@/ui/primitives/table' import { useDashboard } from '../../context' import { TeamAvatar } from '../../sidebar/team-avatar' -import WebhookAddEditDialog from './add-edit-dialog' import { WEBHOOK_EVENT_LABELS, WEBHOOK_EVENTS } from './constants' import WebhookDeleteDialog from './delete-dialog' import WebhookEditSecretDialog from './edit-secret-dialog' import type { Webhook } from './types' +import { UpsertWebhookDialog } from './upsert-webhook-dialog' type WebhookRowProps = { webhook: Webhook @@ -60,11 +60,11 @@ const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { - + e.preventDefault()}> Edit - + e.preventDefault()}> Rotate Secret diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx similarity index 97% rename from src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx rename to src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx index c6126758e..e7a1f51b5 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx @@ -26,7 +26,7 @@ import { type WebhookEvent, } from './constants' -type WebhookAddEditDialogStepsProps = { +type UpsertWebhookDialogStepsProps = { currentStep: number form: UseFormReturn isLoading: boolean @@ -35,7 +35,7 @@ type WebhookAddEditDialogStepsProps = { allEventsSelected: boolean handleAllToggle: () => void handleEventToggle: (event: string) => void - mode: 'add' | 'edit' + mode: 'create' | 'update' hasCopied: boolean onCopied: () => void } @@ -75,7 +75,7 @@ const WebhookExamplePayload = ({ eventType }: { eventType: WebhookEvent }) => ( ) -export function WebhookAddEditDialogSteps({ +export function UpsertWebhookDialogSteps({ currentStep, form, isLoading, @@ -87,7 +87,7 @@ export function WebhookAddEditDialogSteps({ mode, hasCopied, onCopied, -}: WebhookAddEditDialogStepsProps) { +}: UpsertWebhookDialogStepsProps) { const [secretType, setSecretType] = useState<'pre-generated' | 'custom'>( 'pre-generated' ) @@ -111,10 +111,10 @@ export function WebhookAddEditDialogSteps({ return () => window.clearTimeout(id) }, [secretType]) - // sync secret with form state and validation - only in 'add' mode - // in 'edit' mode, we should never touch the signature secret + // sync secret with form state and validation - only in create mode + // in update mode, we should never touch the signature secret useEffect(() => { - if (mode !== 'add') return + if (mode !== 'create') return if (secretType === 'pre-generated') { // set pre-generated secret and trigger validation to clear any errors diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx similarity index 91% rename from src/features/dashboard/settings/webhooks/add-edit-dialog.tsx rename to src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx index 65fa50e1d..5df6644a1 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx @@ -27,28 +27,28 @@ import { Form } from '@/ui/primitives/form' import { AddIcon, CheckIcon } from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader' import { useDashboard } from '../../context' -import { WebhookAddEditDialogSteps } from './add-edit-dialog-steps' import { WEBHOOK_EVENTS, type WebhookEvent } from './constants' import { FinishWebhookSetupDialog } from './finish-webhook-setup-dialog' import type { Webhook } from './types' +import { UpsertWebhookDialogSteps } from './upsert-webhook-dialog-steps' -type WebhookAddEditDialogProps = +type UpsertWebhookDialogProps = | { children: React.ReactNode - mode: 'add' + mode: 'create' webhook?: undefined } | { children: React.ReactNode - mode: 'edit' + mode: 'update' webhook: Webhook } -export default function WebhookAddEditDialog({ +export function UpsertWebhookDialog({ children: trigger, mode, webhook, -}: WebhookAddEditDialogProps) { +}: UpsertWebhookDialogProps) { 'use no memo' const { team } = useDashboard() @@ -61,20 +61,20 @@ export default function WebhookAddEditDialog({ const [hasCopied, setHasCopied] = useState(false) const [finishSetupDialogOpen, setFinishSetupDialogOpen] = useState(false) - const isEditMode = mode === 'edit' - const totalSteps = isEditMode ? 1 : 2 + const isUpdateMode = mode === 'update' + const totalSteps = isUpdateMode ? 1 : 2 const listQueryKey = trpc.webhooks.list.queryOptions({ teamSlug: team.slug, }).queryKey const defaultValues: UpsertWebhookInput = { - webhookId: isEditMode ? webhook?.id : undefined, + webhookId: isUpdateMode ? webhook?.id : undefined, mode, name: webhook?.name || '', url: webhook?.url || '', events: webhook?.events || [], - ...(isEditMode ? {} : { signatureSecret: '' }), + ...(isUpdateMode ? {} : { signatureSecret: '' }), } const form = useForm({ @@ -90,14 +90,14 @@ export default function WebhookAddEditDialog({ onSuccess: () => { toast( defaultSuccessToast( - isEditMode + isUpdateMode ? 'Webhook updated successfully' : 'Webhook created successfully' ) ) void queryClient.invalidateQueries({ queryKey: listQueryKey }) - if (!isEditMode) { + if (!isUpdateMode) { handleDialogChange(false) setFinishSetupDialogOpen(true) return @@ -109,7 +109,7 @@ export default function WebhookAddEditDialog({ toast( defaultErrorToast( err.message || - (isEditMode + (isUpdateMode ? 'Failed to update webhook' : 'Failed to create webhook') ) @@ -211,9 +211,9 @@ export default function WebhookAddEditDialog({ - {isEditMode ? 'Edit Webhook' : 'Add Webhook'} + {isUpdateMode ? 'Edit Webhook' : 'Add Webhook'} - {!isEditMode && ( + {!isUpdateMode && (
Step {currentStep} / {totalSteps} @@ -230,7 +230,7 @@ export default function WebhookAddEditDialog({
- - {isEditMode ? 'Saving Changes...' : 'Adding Webhook...'} + {isUpdateMode ? 'Saving Changes...' : 'Adding Webhook...'}
- ) : isEditMode ? ( + ) : isUpdateMode ? ( - +
From 2db48f4a7ff68847d3d79c880b4c1bd336d83f16 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 13:40:08 -0400 Subject: [PATCH 18/90] refactor: update UserAvatar component to use 'label' prop instead of 'email' - Changed the UserAvatar component to accept a 'label' prop for displaying user information, enhancing clarity in the codebase. - Updated instances of UserAvatar across member and API keys table rows to reflect this change, ensuring consistency in the usage of the component. - Removed the TeamAvatar component in favor of UserAvatar for better uniformity in the dashboard's UI. These changes aim to improve the maintainability and readability of the code related to user avatars. --- src/features/dashboard/members/member-table-row.tsx | 2 +- .../dashboard/settings/keys/api-keys-table-row.tsx | 2 +- src/features/dashboard/settings/webhooks/table-row.tsx | 10 ++-------- src/features/dashboard/shared/user-avatar.tsx | 6 +++--- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/features/dashboard/members/member-table-row.tsx b/src/features/dashboard/members/member-table-row.tsx index 2834f16fa..3496ae4be 100644 --- a/src/features/dashboard/members/member-table-row.tsx +++ b/src/features/dashboard/members/member-table-row.tsx @@ -257,7 +257,7 @@ const AddedCell = ({ ) : ( )} {showRemove ? ( diff --git a/src/features/dashboard/settings/keys/api-keys-table-row.tsx b/src/features/dashboard/settings/keys/api-keys-table-row.tsx index 4c82af330..59e7341ef 100644 --- a/src/features/dashboard/settings/keys/api-keys-table-row.tsx +++ b/src/features/dashboard/settings/keys/api-keys-table-row.tsx @@ -123,7 +123,7 @@ export const ApiKeysTableRow = ({ apiKey, onDelete }: ApiKeysTableRowProps) => { - + {createdByEmail} diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index ad5ae46c3..cc88bb718 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -20,7 +20,7 @@ import { } from '@/ui/primitives/icons' import { TableCell, TableRow } from '@/ui/primitives/table' import { useDashboard } from '../../context' -import { TeamAvatar } from '../../sidebar/team-avatar' +import { UserAvatar } from '../../shared' import { WEBHOOK_EVENT_LABELS, WEBHOOK_EVENTS } from './constants' import WebhookDeleteDialog from './delete-dialog' import WebhookEditSecretDialog from './edit-secret-dialog' @@ -135,13 +135,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => {

{createdAt}

- +
diff --git a/src/features/dashboard/shared/user-avatar.tsx b/src/features/dashboard/shared/user-avatar.tsx index 5738138ed..e17ba8825 100644 --- a/src/features/dashboard/shared/user-avatar.tsx +++ b/src/features/dashboard/shared/user-avatar.tsx @@ -4,16 +4,16 @@ import { cn } from '@/lib/utils' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' interface UserAvatarProps { - email?: string | null + label?: string | null url?: string | null className?: string } -export const UserAvatar = ({ email, url, className }: UserAvatarProps) => ( +export const UserAvatar = ({ label, url, className }: UserAvatarProps) => ( - {email?.charAt(0).toUpperCase() ?? '?'} + {label?.charAt(0).toUpperCase() ?? '?'} ) From ce17dd8ee1f3c02cd563515398e952719ad93614 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 13:50:33 -0400 Subject: [PATCH 19/90] refactor: enhance WebhookTableRow with URL copy functionality - Introduced a new WebhookNameAndUrl component to encapsulate the display and copy functionality for webhook names and URLs. - Implemented clipboard copying with visual feedback for the URL, improving user interaction. - Updated WebhookTableRow to utilize the new component, streamlining the code and enhancing maintainability. These changes aim to improve the user experience when managing webhooks by making URL copying more intuitive. --- .../dashboard/settings/webhooks/table-row.tsx | 74 ++++++++++++++++--- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index cc88bb718..ae48206c3 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,8 +1,11 @@ 'use client' import { useState } from 'react' +import { useClipboard } from '@/lib/hooks/use-clipboard' +import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { Badge } from '@/ui/primitives/badge' +import { Button } from '@/ui/primitives/button' import { DropdownMenu, DropdownMenuContent, @@ -12,6 +15,8 @@ import { } from '@/ui/primitives/dropdown-menu' import { IconButton } from '@/ui/primitives/icon-button' import { + CheckIcon, + CopyIcon, EditIcon, IndicatorDotsIcon, PrivateIcon, @@ -36,6 +41,62 @@ type WebhookRowActionsProps = { webhook: Webhook } +type WebhookNameAndUrlProps = { + name: string + url: string +} + +type UrlIconState = 'copied' | 'hovered' | 'idle' + +const urlIconMap: Record = { + copied: CheckIcon, + hovered: CopyIcon, + idle: WebhookIcon, +} + +const WebhookNameAndUrl = ({ name, url }: WebhookNameAndUrlProps) => { + const [wasCopied, copy] = useClipboard(1500) + const [isUrlHovered, setIsUrlHovered] = useState(false) + const iconState: UrlIconState = wasCopied + ? 'copied' + : isUrlHovered + ? 'hovered' + : 'idle' + const UrlIcon = urlIconMap[iconState] + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation() + await copy(url) + toast(defaultSuccessToast('Webhook URL copied')) + } + + return ( + <> + + +
+

{name}

+ +
+ + ) +} + const rowCellClassName = 'h-[50px] p-0 align-top' const rowContentClassName = 'flex h-11 items-center' const actionIconClassName = 'size-4 text-fg-tertiary' @@ -101,18 +162,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => {
-
- -
- -
-

- {webhook.name} -

-

- {webhook.url} -

-
+
From 1c45c47981402ac494b8f759584458e7fc43e30c Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 13:59:48 -0400 Subject: [PATCH 20/90] refactor: enhance WebhookTableRow with event badges and tooltips - Introduced a new WebhookEventBadges component to display webhook events with tooltips for better user interaction. - Replaced inline event badge rendering in WebhookTableRow with the new component, improving code readability and maintainability. - Added tooltip functionality to provide additional context for webhook events, enhancing the user experience. These changes aim to improve the clarity and usability of the webhook management interface. --- .../dashboard/settings/webhooks/table-row.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index ae48206c3..1e1539935 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { Fragment, useState } from 'react' import { useClipboard } from '@/lib/hooks/use-clipboard' import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' @@ -24,6 +24,11 @@ import { WebhookIcon, } from '@/ui/primitives/icons' import { TableCell, TableRow } from '@/ui/primitives/table' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/ui/primitives/tooltip' import { useDashboard } from '../../context' import { UserAvatar } from '../../shared' import { WEBHOOK_EVENT_LABELS, WEBHOOK_EVENTS } from './constants' @@ -109,6 +114,42 @@ const getWebhookEventLabel = (event: string): string => { return WEBHOOK_EVENT_LABELS[matchedEvent] } +type WebhookEventBadgesProps = { + events: readonly string[] +} + +const WebhookEventBadges = ({ events }: WebhookEventBadgesProps) => { + const isAllEvents = events.length === WEBHOOK_EVENTS.length + + if (isAllEvents) { + return ( + + + ALL ({events.length}) + + +
+ {WEBHOOK_EVENTS.map((event, index) => ( + + {index > 0 && ( + + )} + {WEBHOOK_EVENT_LABELS[event]} + + ))} +
+
+
+ ) + } + + return events.map((event) => ( + {getWebhookEventLabel(event)} + )) +} + const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { const [dropDownOpen, setDropDownOpen] = useState(false) @@ -172,11 +213,7 @@ export const WebhookTableRow = ({ webhook, className }: WebhookRowProps) => {
- {webhook.events.map((event) => ( - - {getWebhookEventLabel(event)} - - ))} +
From 8bf085de33d86c6b54509e393718e0c60611a4ab Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 14:42:04 -0400 Subject: [PATCH 21/90] feat: introduce SandboxLifecycleEventType for webhook management - Added a new schema for SandboxLifecycleEventType to standardize lifecycle event types related to sandboxes. - Updated constants and components to utilize the new schema, enhancing type safety and maintainability. - Refactored webhook-related components to replace hardcoded event strings with the new type, improving clarity and reducing potential errors. These changes aim to streamline the webhook management process and ensure consistent handling of sandbox lifecycle events. --- .../sandboxes/lifecycle-event-types.ts | 13 ++++++++++ .../dashboard/settings/webhooks/constants.ts | 12 ++------- .../dashboard/settings/webhooks/table-row.tsx | 10 ++++--- .../webhooks/upsert-webhook-dialog-steps.tsx | 21 ++++++++------- .../webhooks/upsert-webhook-dialog.tsx | 26 ++++++++++++------- 5 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 src/core/modules/sandboxes/lifecycle-event-types.ts diff --git a/src/core/modules/sandboxes/lifecycle-event-types.ts b/src/core/modules/sandboxes/lifecycle-event-types.ts new file mode 100644 index 000000000..3efec7212 --- /dev/null +++ b/src/core/modules/sandboxes/lifecycle-event-types.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +const SandboxLifecycleEventTypeSchema = z.enum([ + 'sandbox.lifecycle.created', + 'sandbox.lifecycle.updated', + 'sandbox.lifecycle.paused', + 'sandbox.lifecycle.resumed', + 'sandbox.lifecycle.killed', +]) + +type SandboxLifecycleEventType = z.infer + +export { SandboxLifecycleEventTypeSchema, type SandboxLifecycleEventType } diff --git a/src/features/dashboard/settings/webhooks/constants.ts b/src/features/dashboard/settings/webhooks/constants.ts index f89ba9988..0b08dc2b9 100644 --- a/src/features/dashboard/settings/webhooks/constants.ts +++ b/src/features/dashboard/settings/webhooks/constants.ts @@ -1,14 +1,6 @@ -export const WEBHOOK_EVENTS = [ - 'sandbox.lifecycle.created', - 'sandbox.lifecycle.paused', - 'sandbox.lifecycle.resumed', - 'sandbox.lifecycle.updated', - 'sandbox.lifecycle.killed', -] as const +import type { SandboxLifecycleEventType } from '@/core/modules/sandboxes/lifecycle-event-types' -export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number] - -export const WEBHOOK_EVENT_LABELS: Record = { +export const WEBHOOK_EVENT_LABELS: Record = { 'sandbox.lifecycle.created': 'CREATE', 'sandbox.lifecycle.paused': 'PAUSE', 'sandbox.lifecycle.resumed': 'RESUME', diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 1e1539935..af2bfcecc 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,6 +1,7 @@ 'use client' import { Fragment, useState } from 'react' +import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' import { useClipboard } from '@/lib/hooks/use-clipboard' import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' @@ -31,7 +32,7 @@ import { } from '@/ui/primitives/tooltip' import { useDashboard } from '../../context' import { UserAvatar } from '../../shared' -import { WEBHOOK_EVENT_LABELS, WEBHOOK_EVENTS } from './constants' +import { WEBHOOK_EVENT_LABELS } from './constants' import WebhookDeleteDialog from './delete-dialog' import WebhookEditSecretDialog from './edit-secret-dialog' import type { Webhook } from './types' @@ -107,7 +108,7 @@ const rowContentClassName = 'flex h-11 items-center' const actionIconClassName = 'size-4 text-fg-tertiary' const getWebhookEventLabel = (event: string): string => { - const matchedEvent = WEBHOOK_EVENTS.find( + const matchedEvent = SandboxLifecycleEventTypeSchema.options.find( (webhookEvent) => webhookEvent === event ) if (!matchedEvent) return event @@ -119,7 +120,8 @@ type WebhookEventBadgesProps = { } const WebhookEventBadges = ({ events }: WebhookEventBadgesProps) => { - const isAllEvents = events.length === WEBHOOK_EVENTS.length + const isAllEvents = + events.length === SandboxLifecycleEventTypeSchema.options.length if (isAllEvents) { return ( @@ -129,7 +131,7 @@ const WebhookEventBadges = ({ events }: WebhookEventBadgesProps) => {
- {WEBHOOK_EVENTS.map((event, index) => ( + {SandboxLifecycleEventTypeSchema.options.map((event, index) => ( {index > 0 && (
) } - -export default WebhooksTable diff --git a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx index ecdf5085a..b692ca1f2 100644 --- a/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx +++ b/src/features/dashboard/settings/webhooks/webhooks-page-content.tsx @@ -8,7 +8,7 @@ import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' import { AddIcon, SearchIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' -import WebhooksTable from './table' +import { WebhooksTable } from './table' import { UpsertWebhookDialog } from './upsert-webhook-dialog' interface WebhooksPageContentProps { From 9494a15e01297edef79c754418c1ec2f3c60d08a Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 15:48:15 -0400 Subject: [PATCH 26/90] refactor: enhance webhook dialogs and table layout for improved usability - Updated the DiscardWebhookChangesDialog to include a size property for the Stay button, enhancing its appearance. - Refined the rowCellClassName in WebhookTableRow to improve styling consistency. - Adjusted headerCellClassName in WebhooksTable to ensure proper padding and alignment. - Modified UpsertWebhookDialog to handle dialog state changes more accurately, preventing accidental discards when in update mode. These changes aim to enhance the user experience and maintainability of the webhook management interface. --- .../settings/webhooks/discard-webhook-changes-dialog.tsx | 6 +++++- src/features/dashboard/settings/webhooks/table-row.tsx | 2 +- src/features/dashboard/settings/webhooks/table.tsx | 6 +++--- .../settings/webhooks/upsert-webhook-dialog.tsx | 9 +++++++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/discard-webhook-changes-dialog.tsx b/src/features/dashboard/settings/webhooks/discard-webhook-changes-dialog.tsx index 7ec00a217..5b1defb7c 100644 --- a/src/features/dashboard/settings/webhooks/discard-webhook-changes-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/discard-webhook-changes-dialog.tsx @@ -34,7 +34,11 @@ export const DiscardWebhookChangesDialog = ({ - - )} + diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index a46987cbf..e6f39abe3 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -28,7 +28,7 @@ export const WebhooksTable = ({ const hasNoWebhooks = totalWebhookCount === 0 const emptyMessage = hasNoWebhooks ? 'No webhooks added yet' - : 'No webhooks match your search.' + : 'No webhooks match your search' return ( diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx index 15cd59f9f..e050b2999 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx @@ -96,7 +96,7 @@ export function UpsertWebhookDialog({ toast( defaultSuccessToast( isUpdateMode - ? 'Webhook updated successfully' + ? 'Webhook edited successfully' : 'Webhook created successfully' ) ) @@ -114,7 +114,7 @@ export function UpsertWebhookDialog({ defaultErrorToast( err.message || (isUpdateMode - ? 'Failed to update webhook' + ? 'Failed to edit webhook' : 'Failed to create webhook') ) ) From 07c17179f2f3674f67a2df33a0f26e98b1b0eb39 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 16:02:44 -0400 Subject: [PATCH 28/90] refactor: replace EditSecretDialog with UpdateWebhookSecretDialog for enhanced secret management - Replaced the EditSecretDialog component with the new UpdateWebhookSecretDialog to improve the user experience when updating webhook secrets. - Updated the WebhookRowActions to reflect this change, ensuring consistent functionality across the application. - Introduced a new dialog that provides better feedback and validation for secret updates, enhancing usability. These changes aim to streamline the process of managing webhook secrets and improve user interaction with the dialogs. --- src/features/dashboard/settings/webhooks/table-row.tsx | 6 +++--- ...ret-dialog.tsx => update-webhook-secret-dialog.tsx} | 10 ++++------ .../settings/webhooks/upsert-webhook-dialog.tsx | 7 +------ 3 files changed, 8 insertions(+), 15 deletions(-) rename src/features/dashboard/settings/webhooks/{edit-secret-dialog.tsx => update-webhook-secret-dialog.tsx} (95%) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index f7d337536..c0b9c824b 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -34,8 +34,8 @@ import { useDashboard } from '../../context' import { UserAvatar } from '../../shared' import { WEBHOOK_EVENT_LABELS } from './constants' import { DeleteWebhookDialog } from './delete-webhook-dialog' -import { EditSecretDialog } from './edit-secret-dialog' import type { Webhook } from './types' +import { UpdateWebhookSecretDialog } from './update-webhook-secret-dialog' import { UpsertWebhookDialog } from './upsert-webhook-dialog' type WebhookRowProps = { @@ -174,11 +174,11 @@ const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { Delete - + e.preventDefault()}> Edit secret - + diff --git a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx b/src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx similarity index 95% rename from src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx rename to src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx index a8b1ba3bf..e2b340b74 100644 --- a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx @@ -37,15 +37,15 @@ import { Input } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader' import type { Webhook } from './types' -interface EditSecretDialogProps { +interface UpdateWebhookSecretDialogProps { children: React.ReactNode webhook: Webhook } -export const EditSecretDialog = ({ +export const UpdateWebhookSecretDialog = ({ children: trigger, webhook, -}: EditSecretDialogProps) => { +}: UpdateWebhookSecretDialogProps) => { 'use no memo' const { team } = useDashboard() @@ -74,9 +74,7 @@ export const EditSecretDialog = ({ handleDialogChange(false) }, onError: (err) => { - toast( - defaultErrorToast(err.message || 'Failed to edit webhook secret') - ) + toast(defaultErrorToast(err.message || 'Failed to edit webhook secret')) }, }) ) diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx index e050b2999..5be723e40 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx @@ -132,12 +132,7 @@ export function UpsertWebhookDialog({ } const handleDialogChange = (value: boolean) => { - if ( - !value && - isUpdateMode && - hasChanges && - !upsertMutation.isPending - ) { + if (!value && isUpdateMode && hasChanges && !upsertMutation.isPending) { setDiscardConfirmOpen(true) return } From 43c62a389582c1c9bae3640c066e20cc0684c2d2 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 16:18:50 -0400 Subject: [PATCH 29/90] refactor: enhance UpsertWebhookDialog and related components for improved secret management - Updated the UpsertWebhookDialog to incorporate a new secret type management system, allowing users to select between pre-generated and custom secrets. - Refactored UpsertWebhookDialogSteps to utilize Zod for schema validation of secret types, enhancing type safety and user feedback. - Improved user interaction by updating button states and handling secret type changes more effectively. These changes aim to streamline the webhook secret management process and enhance the overall user experience in the dialogs. --- .../webhooks/update-webhook-secret-dialog.tsx | 2 +- .../webhooks/upsert-webhook-dialog-steps.tsx | 33 +++++++++++-------- .../webhooks/upsert-webhook-dialog.tsx | 19 +++++++++-- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx b/src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx index e2b340b74..49eec522d 100644 --- a/src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/update-webhook-secret-dialog.tsx @@ -158,7 +158,7 @@ export const UpdateWebhookSecretDialog = ({ {isLoading ? ( <> - Updating secret... + Editing secret... ) : ( <> diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx index 32e7b0d48..066b55d38 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx @@ -1,8 +1,9 @@ 'use client' import { AnimatePresence, motion } from 'motion/react' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef } from 'react' import type { UseFormReturn } from 'react-hook-form' +import { z } from 'zod' import { type SandboxLifecycleEventType, SandboxLifecycleEventTypeSchema, @@ -25,6 +26,10 @@ import { Separator } from '@/ui/primitives/separator' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' import { WEBHOOK_DOCS_URL, WEBHOOK_EVENT_LABELS } from './constants' +const SecretTypeSchema = z.enum(['pre-generated', 'custom']) + +export type SecretType = z.infer + type UpsertWebhookDialogStepsProps = { currentStep: number form: UseFormReturn @@ -35,6 +40,8 @@ type UpsertWebhookDialogStepsProps = { handleAllToggle: () => void handleEventToggle: (event: string) => void mode: 'create' | 'update' + secretType: SecretType + onSecretTypeChange: (value: SecretType) => void hasCopied: boolean onCopied: () => void } @@ -88,13 +95,11 @@ export function UpsertWebhookDialogSteps({ handleAllToggle, handleEventToggle, mode, + secretType, + onSecretTypeChange, hasCopied, onCopied, }: UpsertWebhookDialogStepsProps) { - const [secretType, setSecretType] = useState<'pre-generated' | 'custom'>( - 'pre-generated' - ) - const preGeneratedSecret = useMemo(() => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' @@ -103,7 +108,7 @@ export function UpsertWebhookDialogSteps({ return Array.from(array, (byte) => chars[byte % chars.length]).join('') }, []) - const [copied, copy] = useClipboard(150) + const [copied, copy] = useClipboard() const customSecretInputRef = useRef(null) useEffect(() => { @@ -135,10 +140,9 @@ export function UpsertWebhookDialogSteps({ } }, [mode, secretType, preGeneratedSecret, form]) - const handleCopy = async () => { - await copy(preGeneratedSecret) - setTimeout(onCopied, 150) - } + useEffect(() => { + if (copied) onCopied() + }, [copied, onCopied]) return ( @@ -283,9 +287,10 @@ export function UpsertWebhookDialogSteps({ {/* Tabs */} - setSecretType(v as 'pre-generated' | 'custom') - } + onValueChange={(v) => { + const parsed = SecretTypeSchema.safeParse(v) + if (parsed.success) onSecretTypeChange(parsed.data) + }} className="min-h-0 w-full flex-1 h-full" > @@ -322,7 +327,7 @@ export function UpsertWebhookDialogSteps({
+ + + + + + + + + + + Event + Status + HTTP + Attempts + Duration + Last attempt + + + + {deliveriesQuery.isLoading ? ( + + ) : groups.length === 0 ? ( + + +

+ {hasActiveFilters + ? 'No deliveries match these filters' + : 'No deliveries yet'} +

+
+ ) : ( + groups.map((group) => { + const attempt = group.latestAttempt + const isSelected = group.eventId === selectedGroup?.eventId + + return ( + setSelectedEventId(group.eventId)} + > + +
+

+ {getEventLabel(group.eventType)} +

+

+ {group.sandboxId} +

+
+
+ + {attempt ? ( + + ) : ( + '-' + )} + + + {attempt + ? formatHttpStatus(attempt.httpStatusCode) + : '-'} + + {group.attemptCount} + + {attempt + ? `${attempt.durationMs.toLocaleString()}ms` + : '-'} + + + {attempt ? formatDateTime(attempt.timestamp) : '-'} + +
+ ) + }) + )} +
+
+ + + + + +
+

+ Showing {groups.length.toLocaleString()} grouped events +

+
+ + +
+
+ + ) +} diff --git a/src/features/dashboard/settings/webhooks/detail/header.tsx b/src/features/dashboard/settings/webhooks/detail/header.tsx new file mode 100644 index 000000000..87200b286 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/header.tsx @@ -0,0 +1,101 @@ +'use client' + +import { useSuspenseQuery } from '@tanstack/react-query' +import Link from 'next/link' +import { PROTECTED_URLS } from '@/configs/urls' +import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' +import { WEBHOOK_EVENT_LABELS } from '@/features/dashboard/settings/webhooks/constants' +import { useTRPC } from '@/trpc/client' +import { Badge } from '@/ui/primitives/badge' +import { Button } from '@/ui/primitives/button' +import { ArrowLeftIcon, WebhookIcon } from '@/ui/primitives/icons' +import { WebhookStatusBadge } from './status-badge' + +type WebhookDetailHeaderProps = { + teamSlug: string + webhookId: string +} + +const formatDate = (value?: string) => { + if (!value) return 'Unknown' + + return new Date(value).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) +} + +const getEventLabel = (event: string) => { + const parsed = SandboxLifecycleEventTypeSchema.safeParse(event) + if (parsed.success) return WEBHOOK_EVENT_LABELS[parsed.data] + + return event +} + +export const WebhookDetailHeader = ({ + teamSlug, + webhookId, +}: WebhookDetailHeaderProps) => { + const trpc = useTRPC() + const { data } = useSuspenseQuery( + trpc.webhooks.get.queryOptions({ teamSlug, webhookId }) + ) + const { webhook } = data + + return ( +
+
+
+ + +
+ +
+
+

+ {webhook.name} +

+ +
+

+ {webhook.url} +

+
+
+
+ +
+

+ Webhook ID +

+

+ {webhook.id} +

+

+ Created {formatDate(webhook.createdAt)} +

+
+
+ +
+ {webhook.events.map((event) => ( + {getEventLabel(event)} + ))} +
+
+ ) +} diff --git a/src/features/dashboard/settings/webhooks/detail/index.ts b/src/features/dashboard/settings/webhooks/detail/index.ts new file mode 100644 index 000000000..c8b5997ac --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/index.ts @@ -0,0 +1,3 @@ +export { WebhookDeliveriesContent } from './deliveries-content' +export { WebhookDetailLayout } from './layout' +export { WebhookOverviewContent } from './overview-content' diff --git a/src/features/dashboard/settings/webhooks/detail/layout.tsx b/src/features/dashboard/settings/webhooks/detail/layout.tsx new file mode 100644 index 000000000..55efba0a2 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/layout.tsx @@ -0,0 +1,41 @@ +'use client' + +import { PROTECTED_URLS } from '@/configs/urls' +import { DashboardTabsList } from '@/ui/dashboard-tabs' +import { ListIcon, TrendIcon } from '@/ui/primitives/icons' +import { WebhookDetailHeader } from './header' + +type WebhookDetailLayoutProps = { + children: React.ReactNode + teamSlug: string + webhookId: string +} + +export const WebhookDetailLayout = ({ + children, + teamSlug, + webhookId, +}: WebhookDetailLayoutProps) => ( +
+ + , + }, + { + id: 'deliveries', + label: 'Event deliveries', + href: PROTECTED_URLS.WEBHOOK_DELIVERIES(teamSlug, webhookId), + icon: , + }, + ]} + /> + {children} +
+) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx new file mode 100644 index 000000000..567cb5524 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -0,0 +1,227 @@ +'use client' + +import { useSuspenseQuery } from '@tanstack/react-query' +import { useMemo, useState } from 'react' +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts' +import { useTRPC } from '@/trpc/client' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/ui/primitives/card' +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@/ui/primitives/chart' +import { + getWebhookStatsRange, + WebhookRangeSelector, + type WebhookStatsRange, +} from './range-selector' + +type WebhookOverviewContentProps = { + teamSlug: string + webhookId: string +} + +type MetricCardProps = { + label: string + value: string + description: string +} + +const deliveryChartConfig = { + total: { + label: 'Total deliveries', + color: 'var(--accent-info-highlight)', + }, + failed: { + label: 'Failed deliveries', + color: 'var(--accent-error-highlight)', + }, +} satisfies ChartConfig + +const latencyChartConfig = { + avgDurationMs: { + label: 'Average duration', + color: 'var(--accent-positive-highlight)', + }, +} satisfies ChartConfig + +const formatBucketLabel = (value: string) => + new Date(value).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + }) + +const MetricCard = ({ label, value, description }: MetricCardProps) => ( + + + + {label} + + + {value} + + + +

{description}

+
+
+) + +const EmptyChartState = ({ label }: { label: string }) => ( +
+ {label} +
+) + +export const WebhookOverviewContent = ({ + teamSlug, + webhookId, +}: WebhookOverviewContentProps) => { + const [range, setRange] = useState('24h') + const trpc = useTRPC() + const rangeBounds = useMemo(() => getWebhookStatsRange(range), [range]) + const { data } = useSuspenseQuery( + trpc.webhooks.getDeliveryStats.queryOptions({ + teamSlug, + webhookId, + ...rangeBounds, + }) + ) + const { stats } = data + const successful = Math.max(stats.total - stats.failed, 0) + const failureRate = + stats.total > 0 + ? `${((stats.failed / stats.total) * 100).toFixed(1)}%` + : '0%' + const hasBuckets = stats.buckets.length > 0 + + return ( +
+
+
+

Overview

+

+ Delivery health and latency for this webhook. +

+
+ +
+ +
+ + + + +
+ +
+ + + Event deliveries + + Total and failed attempts over time + + + + {hasBuckets ? ( + + + + + + } /> + + + + + ) : ( + + )} + + + + + + Response time + Average latency in milliseconds + + + {hasBuckets ? ( + + + + + + } /> + + + + ) : ( + + )} + + +
+
+ ) +} diff --git a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx new file mode 100644 index 000000000..f859498d2 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx @@ -0,0 +1,71 @@ +'use client' + +import { z } from 'zod' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/ui/primitives/select' + +export type WebhookStatsRange = '24h' | '7d' | '30d' + +const WebhookStatsRangeSchema = z.enum(['24h', '7d', '30d']) + +export const WEBHOOK_STATS_RANGE_LABELS: Record = { + '24h': 'Last 24 hours', + '7d': 'Last 7 days', + '30d': 'Last 30 days', +} + +const WEBHOOK_STATS_RANGE_HOURS: Record = { + '24h': 24, + '7d': 24 * 7, + '30d': 24 * 30, +} + +type WebhookRangeSelectorProps = { + value: WebhookStatsRange + onChange: (value: WebhookStatsRange) => void +} + +// Builds ISO stats bounds from a range, e.g. "24h" -> { start: "...", end: "..." }. +export const getWebhookStatsRange = (range: WebhookStatsRange) => { + const end = new Date() + const start = new Date( + end.getTime() - WEBHOOK_STATS_RANGE_HOURS[range] * 60 * 60 * 1000 + ) + + return { + start: start.toISOString(), + end: end.toISOString(), + } +} + +export const WebhookRangeSelector = ({ + value, + onChange, +}: WebhookRangeSelectorProps) => { + const handleValueChange = (nextValue: string) => { + const parsed = WebhookStatsRangeSchema.safeParse(nextValue) + if (!parsed.success) return + + onChange(parsed.data) + } + + return ( + + ) +} diff --git a/src/features/dashboard/settings/webhooks/detail/status-badge.tsx b/src/features/dashboard/settings/webhooks/detail/status-badge.tsx new file mode 100644 index 000000000..91a54a39d --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/status-badge.tsx @@ -0,0 +1,36 @@ +import { Badge } from '@/ui/primitives/badge' + +type WebhookDeliveryHealth = 'disabled' | 'failing' | 'healthy' | 'unknown' + +const statusConfigMap: Record< + WebhookDeliveryHealth, + { label: string; variant: React.ComponentProps['variant'] } +> = { + disabled: { label: 'Disabled', variant: 'warning' }, + failing: { label: 'Failing', variant: 'error' }, + healthy: { label: 'Healthy', variant: 'positive' }, + unknown: { label: 'No deliveries', variant: 'info' }, +} + +type WebhookStatusBadgeProps = { + enabled: boolean + failedCount?: number + totalCount?: number +} + +export const WebhookStatusBadge = ({ + enabled, + failedCount, + totalCount, +}: WebhookStatusBadgeProps) => { + const health: WebhookDeliveryHealth = !enabled + ? 'disabled' + : !totalCount + ? 'unknown' + : failedCount && failedCount > 0 + ? 'failing' + : 'healthy' + const config = statusConfigMap[health] + + return {config.label} +} diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index c0b9c824b..7ed470362 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,6 +1,8 @@ 'use client' +import { useRouter } from 'next/navigation' import { Fragment, useState } from 'react' +import { PROTECTED_URLS } from '@/configs/urls' import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' import { useClipboard } from '@/lib/hooks/use-clipboard' import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' @@ -187,6 +189,7 @@ const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { export const WebhookTableRow = ({ webhook }: WebhookRowProps) => { const { team } = useDashboard() + const router = useRouter() const createdAt = webhook.createdAt ? new Date(webhook.createdAt).toLocaleDateString('en-US', { @@ -196,8 +199,28 @@ export const WebhookTableRow = ({ webhook }: WebhookRowProps) => { }) : '-' + const webhookHref = PROTECTED_URLS.WEBHOOK(team.slug, webhook.id) + + const openWebhook = () => { + router.push(webhookHref) + } + + const handleRowKeyDown = ( + event: React.KeyboardEvent + ) => { + if (event.key !== 'Enter' && event.key !== ' ') return + + event.preventDefault() + openWebhook() + } + return ( - + @@ -217,7 +240,10 @@ export const WebhookTableRow = ({ webhook }: WebhookRowProps) => { - + event.stopPropagation()} + > From 757dbe806cba91f345e686d911dafc822181b5d0 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 13 May 2026 13:56:17 -0400 Subject: [PATCH 40/90] refactor(webhooks): simplify stats range handling and improve overview page - Removed redundant default stats range calculation in favor of a dedicated utility function. - Updated the WebhookOverviewPage to utilize the new getWebhookStatsRange function for better clarity. - Enhanced WebhookOverviewContent to accept initial range bounds as a prop, improving flexibility. - Refactored WebhookRangeSelector to handle range changes more effectively. These changes aim to streamline the management of webhook statistics and improve code maintainability. --- .../webhooks/[webhookId]/(tabs)/layout.tsx | 9 +---- .../[webhookId]/(tabs)/overview/page.tsx | 22 +++++----- .../webhooks/detail/overview-content.tsx | 18 ++++++--- .../webhooks/detail/range-selector.tsx | 31 ++------------ .../settings/webhooks/detail/stats-range.ts | 40 +++++++++++++++++++ 5 files changed, 67 insertions(+), 53 deletions(-) create mode 100644 src/features/dashboard/settings/webhooks/detail/stats-range.ts diff --git a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/layout.tsx b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/layout.tsx index f2958fcaa..af168afba 100644 --- a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/layout.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/layout.tsx @@ -21,14 +21,7 @@ export default async function WebhookTabsLayout({ const { teamSlug, webhookId } = await params - prefetch( - trpc.webhooks.get.queryOptions( - { teamSlug, webhookId }, - { - retry: false, - } - ) - ) + prefetch(trpc.webhooks.get.queryOptions({ teamSlug, webhookId })) return ( diff --git a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx index 6cf7d4b82..c68ef5028 100644 --- a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx @@ -1,4 +1,5 @@ import { WebhookOverviewContent } from '@/features/dashboard/settings/webhooks/detail' +import { getWebhookStatsRange } from '@/features/dashboard/settings/webhooks/detail/stats-range' import { prefetch, trpc } from '@/trpc/server' type WebhookOverviewPageProps = { @@ -8,22 +9,11 @@ type WebhookOverviewPageProps = { }> } -// Builds the initial stats range, e.g. now -> last 24 hours. -const getDefaultStatsRange = () => { - const end = new Date() - const start = new Date(end.getTime() - 24 * 60 * 60 * 1000) - - return { - start: start.toISOString(), - end: end.toISOString(), - } -} - export default async function WebhookOverviewPage({ params, }: WebhookOverviewPageProps) { const { teamSlug, webhookId } = await params - const range = getDefaultStatsRange() + const range = getWebhookStatsRange('24h') prefetch( trpc.webhooks.getDeliveryStats.queryOptions({ @@ -33,5 +23,11 @@ export default async function WebhookOverviewPage({ }) ) - return + return ( + + ) } diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index 567cb5524..b9c697833 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -1,7 +1,7 @@ 'use client' import { useSuspenseQuery } from '@tanstack/react-query' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts' import { useTRPC } from '@/trpc/client' import { @@ -17,15 +17,17 @@ import { ChartTooltip, ChartTooltipContent, } from '@/ui/primitives/chart' +import { WebhookRangeSelector } from './range-selector' import { getWebhookStatsRange, - WebhookRangeSelector, type WebhookStatsRange, -} from './range-selector' + type WebhookStatsRangeBounds, +} from './stats-range' type WebhookOverviewContentProps = { teamSlug: string webhookId: string + initialRangeBounds: WebhookStatsRangeBounds } type MetricCardProps = { @@ -84,10 +86,12 @@ const EmptyChartState = ({ label }: { label: string }) => ( export const WebhookOverviewContent = ({ teamSlug, webhookId, + initialRangeBounds, }: WebhookOverviewContentProps) => { const [range, setRange] = useState('24h') + const [rangeBounds, setRangeBounds] = + useState(initialRangeBounds) const trpc = useTRPC() - const rangeBounds = useMemo(() => getWebhookStatsRange(range), [range]) const { data } = useSuspenseQuery( trpc.webhooks.getDeliveryStats.queryOptions({ teamSlug, @@ -102,6 +106,10 @@ export const WebhookOverviewContent = ({ ? `${((stats.failed / stats.total) * 100).toFixed(1)}%` : '0%' const hasBuckets = stats.buckets.length > 0 + const handleRangeChange = (nextRange: WebhookStatsRange) => { + setRange(nextRange) + setRangeBounds(getWebhookStatsRange(nextRange)) + } return (
@@ -112,7 +120,7 @@ export const WebhookOverviewContent = ({ Delivery health and latency for this webhook.

- +
diff --git a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx index f859498d2..2433e3633 100644 --- a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx +++ b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx @@ -8,41 +8,18 @@ import { SelectTrigger, SelectValue, } from '@/ui/primitives/select' - -export type WebhookStatsRange = '24h' | '7d' | '30d' +import { + WEBHOOK_STATS_RANGE_LABELS, + type WebhookStatsRange, +} from './stats-range' const WebhookStatsRangeSchema = z.enum(['24h', '7d', '30d']) -export const WEBHOOK_STATS_RANGE_LABELS: Record = { - '24h': 'Last 24 hours', - '7d': 'Last 7 days', - '30d': 'Last 30 days', -} - -const WEBHOOK_STATS_RANGE_HOURS: Record = { - '24h': 24, - '7d': 24 * 7, - '30d': 24 * 30, -} - type WebhookRangeSelectorProps = { value: WebhookStatsRange onChange: (value: WebhookStatsRange) => void } -// Builds ISO stats bounds from a range, e.g. "24h" -> { start: "...", end: "..." }. -export const getWebhookStatsRange = (range: WebhookStatsRange) => { - const end = new Date() - const start = new Date( - end.getTime() - WEBHOOK_STATS_RANGE_HOURS[range] * 60 * 60 * 1000 - ) - - return { - start: start.toISOString(), - end: end.toISOString(), - } -} - export const WebhookRangeSelector = ({ value, onChange, diff --git a/src/features/dashboard/settings/webhooks/detail/stats-range.ts b/src/features/dashboard/settings/webhooks/detail/stats-range.ts new file mode 100644 index 000000000..85754c499 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/detail/stats-range.ts @@ -0,0 +1,40 @@ +type WebhookStatsRange = '24h' | '7d' | '30d' + +type WebhookStatsRangeBounds = { + start: string + end: string +} + +const WEBHOOK_STATS_RANGE_LABELS: Record = { + '24h': 'Last 24 hours', + '7d': 'Last 7 days', + '30d': 'Last 30 days', +} + +const WEBHOOK_STATS_RANGE_HOURS: Record = { + '24h': 24, + '7d': 24 * 7, + '30d': 24 * 30, +} + +// Builds ISO stats bounds from a range, e.g. "24h" -> { start: "...", end: "..." }. +const getWebhookStatsRange = ( + range: WebhookStatsRange +): WebhookStatsRangeBounds => { + const end = new Date() + const start = new Date( + end.getTime() - WEBHOOK_STATS_RANGE_HOURS[range] * 60 * 60 * 1000 + ) + + return { + start: start.toISOString(), + end: end.toISOString(), + } +} + +export { + getWebhookStatsRange, + WEBHOOK_STATS_RANGE_LABELS, + type WebhookStatsRange, + type WebhookStatsRangeBounds, +} From 2d34a6bd044eb5fe3181cdc72770c2770b15ff00 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 13 May 2026 14:04:50 -0400 Subject: [PATCH 41/90] refactor(webhooks): update WebhookTableRow to use Link component - Replaced useRouter with Link for navigation in WebhookTableRow, enhancing accessibility and simplifying the code. - Modified WebhookNameAndUrl component to accept an href prop for direct linking. - Improved the overall structure and readability of the webhook table row component. These changes aim to streamline navigation and improve user experience in the dashboard. --- .../dashboard/settings/webhooks/table-row.tsx | 46 ++++++++----------- src/ui/primitives/icons.tsx | 40 ++++++++-------- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 7ed470362..9e3983b90 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,6 +1,6 @@ 'use client' -import { useRouter } from 'next/navigation' +import Link from 'next/link' import { Fragment, useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' @@ -49,6 +49,7 @@ type WebhookRowActionsProps = { } type WebhookNameAndUrlProps = { + href: string name: string url: string } @@ -61,7 +62,7 @@ const urlIconMap: Record = { idle: WebhookIcon, } -const WebhookNameAndUrl = ({ name, url }: WebhookNameAndUrlProps) => { +const WebhookNameAndUrl = ({ href, name, url }: WebhookNameAndUrlProps) => { const [wasCopied, copy] = useClipboard(1500) const [isUrlHovered, setIsUrlHovered] = useState(false) const iconState: UrlIconState = wasCopied @@ -87,7 +88,14 @@ const WebhookNameAndUrl = ({ name, url }: WebhookNameAndUrlProps) => {
-

{name}

+
- event.stopPropagation()} - > +
diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons.tsx index 126e7797a..a24432e42 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons.tsx @@ -1048,8 +1048,8 @@ export const BugIcon = ({ className, ...props }: IconProps) => ( ) @@ -1065,14 +1065,14 @@ export const FeedbackIcon = ({ className, ...props }: IconProps) => ( ) @@ -1216,21 +1216,21 @@ export const UnlockIcon = ({ className, ...props }: IconProps) => ( ) @@ -1456,14 +1456,14 @@ export const ArrowRightIcon = ({ className, ...props }: IconProps) => ( ) @@ -1479,14 +1479,14 @@ export const ArrowLeftIcon = ({ className, ...props }: IconProps) => ( ) From 23dccee175a59be793b966d96847a2cde55813d9 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 13 May 2026 16:21:05 -0400 Subject: [PATCH 42/90] feat(webhooks): enhance webhook details and event display - Introduced WebhookEventBadges component to visually represent webhook events in the dashboard. - Updated WebhookDetailHeader to include a badge for the webhook ID with a copy functionality. - Refactored DashboardLayoutHeader to conditionally render webhook titles based on the route. - Added ToolboxComponent to the monitoring chart for improved user interaction. These changes aim to improve the user experience by providing clearer information and better interaction options for webhook management. --- src/configs/layout.ts | 5 +- src/features/dashboard/layouts/header.tsx | 49 +++++++- .../monitoring-sandbox-metrics-chart.tsx | 2 + .../settings/webhooks/detail/header.tsx | 110 ++++++++---------- .../settings/webhooks/event-badges.tsx | 54 +++++++++ .../dashboard/settings/webhooks/table-row.tsx | 56 +-------- 6 files changed, 155 insertions(+), 121 deletions(-) create mode 100644 src/features/dashboard/settings/webhooks/event-badges.tsx diff --git a/src/configs/layout.ts b/src/configs/layout.ts index 632cdfde9..ce22f9ac7 100644 --- a/src/configs/layout.ts +++ b/src/configs/layout.ts @@ -98,8 +98,6 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< '/dashboard/*/webhooks/*/*': (pathname) => { const parts = pathname.split('/') const teamSlug = parts[2]! - const webhookId = parts[4]! - const webhookIdSliced = `${webhookId.slice(0, 6)}...${webhookId.slice(-6)}` return { title: [ @@ -107,10 +105,9 @@ const DASHBOARD_LAYOUT_CONFIGS: Record< label: 'Webhooks', href: PROTECTED_URLS.WEBHOOKS(teamSlug), }, - { label: `Webhook ${webhookIdSliced}` }, + { label: 'Webhook' }, ], type: 'custom', - copyValue: webhookId, custom: { includeHeaderBottomStyles: true, }, diff --git a/src/features/dashboard/layouts/header.tsx b/src/features/dashboard/layouts/header.tsx index 25ac0834e..3ccd42258 100644 --- a/src/features/dashboard/layouts/header.tsx +++ b/src/features/dashboard/layouts/header.tsx @@ -1,10 +1,13 @@ 'use client' +import { useSuspenseQuery } from '@tanstack/react-query' import Link from 'next/link' import { usePathname } from 'next/navigation' import { Fragment } from 'react' import { getDashboardLayoutConfig, type TitleSegment } from '@/configs/layout' +import { PROTECTED_URLS } from '@/configs/urls' import { cn } from '@/lib/utils' +import { useTRPC } from '@/trpc/client' import ClientOnly from '@/ui/client-only' import CopyButton from '@/ui/copy-button' import { SidebarTrigger } from '@/ui/primitives/sidebar' @@ -22,6 +25,7 @@ export default function DashboardLayoutHeader({ const pathname = usePathname() const config = getDashboardLayoutConfig(pathname) const copyableValue = config.copyValue ?? null + const webhookRoute = getWebhookRoute(pathname) return (

- + {webhookRoute ? ( + + ) : ( + + )}

{copyableValue && ( { + const parts = pathname.split('/') + const teamSlug = parts[2] + const resource = parts[3] + const webhookId = parts[4] + + if (resource !== 'webhooks' || !teamSlug || !webhookId) return null + return { teamSlug, webhookId } +} + +function WebhookHeaderTitle({ + teamSlug, + webhookId, +}: { + teamSlug: string + webhookId: string +}) { + const trpc = useTRPC() + const { data } = useSuspenseQuery( + trpc.webhooks.get.queryOptions({ teamSlug, webhookId }) + ) + + return ( + + + Webhooks + + / + {data.webhook.name} + + ) +} + function HeaderTitle({ title }: { title: string | TitleSegment[] }) { if (typeof title === 'string') { return title diff --git a/src/features/dashboard/sandbox/monitoring/components/monitoring-sandbox-metrics-chart.tsx b/src/features/dashboard/sandbox/monitoring/components/monitoring-sandbox-metrics-chart.tsx index 2cdd149f4..b6392a9bd 100644 --- a/src/features/dashboard/sandbox/monitoring/components/monitoring-sandbox-metrics-chart.tsx +++ b/src/features/dashboard/sandbox/monitoring/components/monitoring-sandbox-metrics-chart.tsx @@ -11,6 +11,7 @@ import { BrushComponent, GridComponent, MarkPointComponent, + ToolboxComponent, } from 'echarts/components' import * as echarts from 'echarts/core' import { SVGRenderer } from 'echarts/renderers' @@ -46,6 +47,7 @@ echarts.use([ GridComponent, BrushComponent, MarkPointComponent, + ToolboxComponent, SVGRenderer, AxisPointerComponent, ]) diff --git a/src/features/dashboard/settings/webhooks/detail/header.tsx b/src/features/dashboard/settings/webhooks/detail/header.tsx index 87200b286..2df47aa38 100644 --- a/src/features/dashboard/settings/webhooks/detail/header.tsx +++ b/src/features/dashboard/settings/webhooks/detail/header.tsx @@ -1,15 +1,14 @@ 'use client' import { useSuspenseQuery } from '@tanstack/react-query' -import Link from 'next/link' -import { PROTECTED_URLS } from '@/configs/urls' -import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' -import { WEBHOOK_EVENT_LABELS } from '@/features/dashboard/settings/webhooks/constants' +import { WebhookEventBadges } from '@/features/dashboard/settings/webhooks/event-badges' +import { useClipboard } from '@/lib/hooks/use-clipboard' +import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' import { useTRPC } from '@/trpc/client' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' -import { ArrowLeftIcon, WebhookIcon } from '@/ui/primitives/icons' -import { WebhookStatusBadge } from './status-badge' +import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' +import { DetailsItem, DetailsRow } from '../../../layouts/details-row' type WebhookDetailHeaderProps = { teamSlug: string @@ -17,7 +16,7 @@ type WebhookDetailHeaderProps = { } const formatDate = (value?: string) => { - if (!value) return 'Unknown' + if (!value) return '-' return new Date(value).toLocaleDateString('en-US', { month: 'short', @@ -26,11 +25,36 @@ const formatDate = (value?: string) => { }) } -const getEventLabel = (event: string) => { - const parsed = SandboxLifecycleEventTypeSchema.safeParse(event) - if (parsed.success) return WEBHOOK_EVENT_LABELS[parsed.data] +const getWebhookIdBadgeLabel = (id: string) => + `${id.slice(0, 6)}...${id.slice(-6)}` - return event +const WebhookIdBadge = ({ id }: { id: string }) => { + const [wasCopied, copy] = useClipboard(1500) + + const handleCopy = async () => { + await copy(id) + toast(defaultSuccessToast('Webhook ID copied')) + } + + return ( + + {getWebhookIdBadgeLabel(id)} + + + ) } export const WebhookDetailHeader = ({ @@ -44,58 +68,20 @@ export const WebhookDetailHeader = ({ const { webhook } = data return ( -
-
-
- - -
- -
-
-

- {webhook.name} -

- -
-

- {webhook.url} -

-
+
+ + + + + +

{formatDate(webhook.createdAt)}

+
+ +
+
-
- -
-

- Webhook ID -

-

- {webhook.id} -

-

- Created {formatDate(webhook.createdAt)} -

-
-
- -
- {webhook.events.map((event) => ( - {getEventLabel(event)} - ))} -
+ +
) } diff --git a/src/features/dashboard/settings/webhooks/event-badges.tsx b/src/features/dashboard/settings/webhooks/event-badges.tsx new file mode 100644 index 000000000..b19df45b7 --- /dev/null +++ b/src/features/dashboard/settings/webhooks/event-badges.tsx @@ -0,0 +1,54 @@ +import { Fragment } from 'react' +import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' +import { Badge } from '@/ui/primitives/badge' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/ui/primitives/tooltip' +import { WEBHOOK_EVENT_LABELS } from './constants' + +type WebhookEventBadgesProps = { + events: readonly string[] +} + +const getWebhookEventLabel = (event: string): string => { + const matchedEvent = SandboxLifecycleEventTypeSchema.options.find( + (webhookEvent) => webhookEvent === event + ) + if (!matchedEvent) return event + return WEBHOOK_EVENT_LABELS[matchedEvent] +} + +export const WebhookEventBadges = ({ events }: WebhookEventBadgesProps) => { + const isAllEvents = + events.length === SandboxLifecycleEventTypeSchema.options.length + + if (isAllEvents) { + return ( + + + ALL ({events.length}) + + +
+ {SandboxLifecycleEventTypeSchema.options.map((event, index) => ( + + {index > 0 && ( + + )} + {WEBHOOK_EVENT_LABELS[event]} + + ))} +
+
+
+ ) + } + + return events.map((event) => ( + {getWebhookEventLabel(event)} + )) +} diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 9e3983b90..ae895e4c2 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,13 +1,11 @@ 'use client' import Link from 'next/link' -import { Fragment, useState } from 'react' +import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' -import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' import { useClipboard } from '@/lib/hooks/use-clipboard' import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' -import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' import { DropdownMenu, @@ -27,15 +25,10 @@ import { WebhookIcon, } from '@/ui/primitives/icons' import { TableCell, TableRow } from '@/ui/primitives/table' -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/ui/primitives/tooltip' import { useDashboard } from '../../context' import { UserAvatar } from '../../shared' -import { WEBHOOK_EVENT_LABELS } from './constants' import { DeleteWebhookDialog } from './delete-webhook-dialog' +import { WebhookEventBadges } from './event-badges' import type { Webhook } from './types' import { UpdateWebhookSecretDialog } from './update-webhook-secret-dialog' import { UpsertWebhookDialog } from './upsert-webhook-dialog' @@ -116,51 +109,6 @@ const rowCellClassName = 'p-0 py-1.5 align-middle [tr:first-child>&]:pt-0' const rowContentClassName = 'flex items-center' const actionIconClassName = 'size-4 text-fg-tertiary' -const getWebhookEventLabel = (event: string): string => { - const matchedEvent = SandboxLifecycleEventTypeSchema.options.find( - (webhookEvent) => webhookEvent === event - ) - if (!matchedEvent) return event - return WEBHOOK_EVENT_LABELS[matchedEvent] -} - -type WebhookEventBadgesProps = { - events: readonly string[] -} - -const WebhookEventBadges = ({ events }: WebhookEventBadgesProps) => { - const isAllEvents = - events.length === SandboxLifecycleEventTypeSchema.options.length - - if (isAllEvents) { - return ( - - - ALL ({events.length}) - - -
- {SandboxLifecycleEventTypeSchema.options.map((event, index) => ( - - {index > 0 && ( - - )} - {WEBHOOK_EVENT_LABELS[event]} - - ))} -
-
-
- ) - } - - return events.map((event) => ( - {getWebhookEventLabel(event)} - )) -} - const WebhookRowActions = ({ webhook }: WebhookRowActionsProps) => { const [dropDownOpen, setDropDownOpen] = useState(false) From a7811d449dbdfe10f42e3ea6248096294f07015e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 13:13:24 -0400 Subject: [PATCH 43/90] refactor(webhooks): enforce delivery stats range validation and update schema - Enhanced GetWebhookDeliveryStatsInputSchema to include a validation rule ensuring the delivery stats range does not exceed 7 days. - Removed '30d' option from WebhookStatsRangeSchema and related components to simplify the selection options. These changes aim to improve data integrity and user experience when selecting webhook delivery statistics. --- src/core/server/functions/webhooks/schema.ts | 24 +++++++++++++++---- .../webhooks/detail/range-selector.tsx | 2 +- .../settings/webhooks/detail/stats-range.ts | 4 +--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index 98d6dec79..e5c296e29 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -65,11 +65,25 @@ export const GetWebhookDeliveryInputSchema = z.object({ deliveryId: z.uuid(), }) -export const GetWebhookDeliveryStatsInputSchema = z.object({ - webhookId: z.uuid(), - start: z.iso.datetime().optional(), - end: z.iso.datetime().optional(), -}) +export const GetWebhookDeliveryStatsInputSchema = z + .object({ + webhookId: z.uuid(), + start: z.iso.datetime().optional(), + end: z.iso.datetime().optional(), + }) + .superRefine((data, ctx) => { + if (!data.start || !data.end) return + + const start = new Date(data.start) + const end = new Date(data.end) + if (end.getTime() - start.getTime() <= 7 * 24 * 60 * 60 * 1000) return + + ctx.addIssue({ + code: 'custom', + message: 'Webhook delivery stats range must be 7 days or less', + path: ['start'], + }) + }) export type UpsertWebhookInput = z.input export type DeleteWebhookInput = z.input diff --git a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx index 2433e3633..2eaf11949 100644 --- a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx +++ b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx @@ -13,7 +13,7 @@ import { type WebhookStatsRange, } from './stats-range' -const WebhookStatsRangeSchema = z.enum(['24h', '7d', '30d']) +const WebhookStatsRangeSchema = z.enum(['24h', '7d']) type WebhookRangeSelectorProps = { value: WebhookStatsRange diff --git a/src/features/dashboard/settings/webhooks/detail/stats-range.ts b/src/features/dashboard/settings/webhooks/detail/stats-range.ts index 85754c499..274aba4e1 100644 --- a/src/features/dashboard/settings/webhooks/detail/stats-range.ts +++ b/src/features/dashboard/settings/webhooks/detail/stats-range.ts @@ -1,4 +1,4 @@ -type WebhookStatsRange = '24h' | '7d' | '30d' +type WebhookStatsRange = '24h' | '7d' type WebhookStatsRangeBounds = { start: string @@ -8,13 +8,11 @@ type WebhookStatsRangeBounds = { const WEBHOOK_STATS_RANGE_LABELS: Record = { '24h': 'Last 24 hours', '7d': 'Last 7 days', - '30d': 'Last 30 days', } const WEBHOOK_STATS_RANGE_HOURS: Record = { '24h': 24, '7d': 24 * 7, - '30d': 24 * 30, } // Builds ISO stats bounds from a range, e.g. "24h" -> { start: "...", end: "..." }. From 854d047cb2ef8c67d110c1246a4e52de1e29ed4b Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 13:54:23 -0400 Subject: [PATCH 44/90] feat(webhooks): implement cursor-based pagination for webhook deliveries - Updated the API and OpenAPI specification to replace offset-based pagination with cursor-based pagination for listing webhook deliveries. - Modified related components and queries to support the new cursor parameter, enhancing the efficiency of data retrieval. - Improved user experience by allowing seamless navigation through delivery records with the addition of a "Load more" button. These changes aim to optimize the performance and usability of webhook delivery monitoring in the dashboard. --- spec/openapi.argus.yaml | 13 ++++--- .../[webhookId]/(tabs)/deliveries/page.tsx | 3 +- .../modules/webhooks/repository.server.ts | 16 ++++++-- src/core/server/api/routers/webhooks.ts | 9 ++--- src/core/server/functions/webhooks/schema.ts | 2 +- src/core/shared/contracts/argus-api.types.ts | 5 ++- .../webhooks/detail/deliveries-content.tsx | 39 ++++++++++--------- 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index e1c8f01e6..4ddb15569 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -675,14 +675,12 @@ paths: Supabase2TeamAuth: [] parameters: - $ref: "#/components/parameters/webhookID" - - name: offset + - name: cursor in: query required: false schema: - type: integer - format: int32 - minimum: 0 - default: 0 + type: string + description: Opaque cursor from the X-Next-Cursor response header. - name: limit in: query required: false @@ -722,6 +720,11 @@ paths: responses: "200": description: List of webhook delivery attempts grouped by event. + headers: + X-Next-Cursor: + description: Cursor to pass to the next list request, omitted when there is no next page. + schema: + type: string content: application/json: schema: diff --git a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/deliveries/page.tsx b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/deliveries/page.tsx index 989610029..f0609258f 100644 --- a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/deliveries/page.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/deliveries/page.tsx @@ -14,11 +14,10 @@ export default async function WebhookDeliveriesPage({ const { teamSlug, webhookId } = await params prefetch( - trpc.webhooks.listDeliveries.queryOptions({ + trpc.webhooks.listDeliveries.infiniteQueryOptions({ teamSlug, webhookId, limit: 25, - offset: 0, deliveryStatus: 'all', }) ) diff --git a/src/core/modules/webhooks/repository.server.ts b/src/core/modules/webhooks/repository.server.ts index 03ec7349c..58a16c3ec 100644 --- a/src/core/modules/webhooks/repository.server.ts +++ b/src/core/modules/webhooks/repository.server.ts @@ -27,12 +27,17 @@ export interface UpsertWebhookInput { export interface ListWebhookDeliveriesInput { webhookId: string limit: number - offset: number + cursor?: string orderAsc: boolean deliveryStatus?: 'success' | 'failed' eventType?: string } +interface ListWebhookDeliveriesResult { + data: ArgusComponents['schemas']['WebhookDeliveryEvent'][] + nextCursor: string | null +} + export interface GetWebhookDeliveryInput { webhookId: string deliveryId: string @@ -53,7 +58,7 @@ export interface WebhooksRepository { ): Promise> listWebhookDeliveries( input: ListWebhookDeliveriesInput - ): Promise> + ): Promise> getWebhookDelivery( input: GetWebhookDeliveryInput ): Promise> @@ -139,7 +144,7 @@ export function createWebhooksRepository( path: { webhookID: input.webhookId }, query: { limit: input.limit, - offset: input.offset, + cursor: input.cursor, orderAsc: input.orderAsc, deliveryStatus, eventType, @@ -158,7 +163,10 @@ export function createWebhooksRepository( ) } - return ok(response.data ?? []) + return ok({ + data: response.data ?? [], + nextCursor: response.response.headers.get('X-Next-Cursor'), + }) }, async getWebhookDelivery(input) { const response = await deps.infraClient.GET( diff --git a/src/core/server/api/routers/webhooks.ts b/src/core/server/api/routers/webhooks.ts index e2d0a4eec..5a591307e 100644 --- a/src/core/server/api/routers/webhooks.ts +++ b/src/core/server/api/routers/webhooks.ts @@ -105,7 +105,7 @@ export const webhooksRouter = createTRPCRouter({ const result = await ctx.webhooksRepository.listWebhookDeliveries({ webhookId: input.webhookId, limit: input.limit, - offset: input.offset, + cursor: input.cursor, orderAsc: input.orderAsc, deliveryStatus: input.deliveryStatus === 'all' ? undefined : input.deliveryStatus, @@ -133,11 +133,8 @@ export const webhooksRouter = createTRPCRouter({ } return { - groups: result.data.map(toDeliveryEventGroup), - pagination: { - limit: input.limit, - offset: input.offset, - }, + groups: result.data.data.map(toDeliveryEventGroup), + nextCursor: result.data.nextCursor, } }), diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index e5c296e29..4ed051aef 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -54,7 +54,7 @@ export const GetWebhookInputSchema = z.object({ export const ListWebhookDeliveriesInputSchema = z.object({ webhookId: z.uuid(), limit: z.number().int().min(1).max(100).optional().default(25), - offset: z.number().int().min(0).optional().default(0), + cursor: z.string().optional(), orderAsc: z.boolean().optional().default(false), deliveryStatus: DeliveryStatusFilterSchema.optional().default('all'), eventType: z.string().trim().min(1).optional(), diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index dc0303a7b..9b33faa5b 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -628,7 +628,8 @@ export interface operations { webhookDeliveriesList: { parameters: { query?: { - offset?: number + /** @description Opaque cursor from the X-Next-Cursor response header. */ + cursor?: string limit?: number orderAsc?: boolean /** @description Filter deliveries by delivery status */ @@ -647,6 +648,8 @@ export interface operations { /** @description List of webhook delivery attempts grouped by event. */ 200: { headers: { + /** @description Cursor to pass to the next list request, omitted when there is no next page. */ + 'X-Next-Cursor'?: string [name: string]: unknown } content: { diff --git a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx index 1e93d7624..0e810d7f7 100644 --- a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx @@ -1,6 +1,10 @@ 'use client' -import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { + keepPreviousData, + useInfiniteQuery, + useQuery, +} from '@tanstack/react-query' import { useMemo, useState } from 'react' import { z } from 'zod' import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' @@ -245,26 +249,28 @@ export const WebhookDeliveriesContent = ({ const [deliveryStatus, setDeliveryStatus] = useState('all') const [eventType, setEventType] = useState('') - const [offset, setOffset] = useState(0) const [selectedEventId, setSelectedEventId] = useState(null) const trpc = useTRPC() const eventTypeFilter = eventType.trim() || undefined - const deliveriesQuery = useQuery( - trpc.webhooks.listDeliveries.queryOptions( + const deliveriesQuery = useInfiniteQuery( + trpc.webhooks.listDeliveries.infiniteQueryOptions( { teamSlug, webhookId, limit: 25, - offset, deliveryStatus, eventType: eventTypeFilter, }, { + getNextPageParam: (page) => page.nextCursor ?? undefined, placeholderData: keepPreviousData, } ) ) - const groups = deliveriesQuery.data?.groups ?? [] + const groups = useMemo( + () => deliveriesQuery.data?.pages.flatMap((page) => page.groups) ?? [], + [deliveriesQuery.data] + ) const selectedGroup = useMemo( () => groups.find((group) => group.eventId === selectedEventId) ?? groups[0], @@ -284,7 +290,6 @@ export const WebhookDeliveriesContent = ({ ) ) const detailedAttempt = deliveryDetailQuery.data?.delivery ?? selectedAttempt - const hasNextPage = groups.length === 25 const hasActiveFilters = deliveryStatus !== 'all' || Boolean(eventTypeFilter) return ( @@ -301,7 +306,7 @@ export const WebhookDeliveriesContent = ({ value={deliveryStatus} onChange={(value) => { setDeliveryStatus(value) - setOffset(0) + setSelectedEventId(null) }} /> { setEventType(event.target.value) - setOffset(0) + setSelectedEventId(null) }} />
@@ -416,17 +421,13 @@ export const WebhookDeliveriesContent = ({
-
From 1e62dc806499400c22981dea70e494ce8996ea6a Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 14:04:11 -0400 Subject: [PATCH 45/90] Fix warning --- .../sandboxes/monitoring/charts/team-metrics-chart/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/index.tsx b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/index.tsx index d0ac1cbd5..2af4f9757 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/index.tsx +++ b/src/features/dashboard/sandboxes/monitoring/charts/team-metrics-chart/index.tsx @@ -12,6 +12,7 @@ import { GridComponent, MarkLineComponent, MarkPointComponent, + ToolboxComponent, TooltipComponent, } from 'echarts/components' import * as echarts from 'echarts/core' @@ -42,6 +43,7 @@ echarts.use([ MarkPointComponent, MarkLineComponent, AxisPointerComponent, + ToolboxComponent, CanvasRenderer, ]) From b2dca642cb6c84cca2a581436c51aaa1f650959e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 14:58:41 -0400 Subject: [PATCH 46/90] Remove unused constants --- .../dashboard/settings/webhooks/constants.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/constants.ts b/src/features/dashboard/settings/webhooks/constants.ts index a91aab359..0b08dc2b9 100644 --- a/src/features/dashboard/settings/webhooks/constants.ts +++ b/src/features/dashboard/settings/webhooks/constants.ts @@ -1,15 +1,5 @@ import type { SandboxLifecycleEventType } from '@/core/modules/sandboxes/lifecycle-event-types' -export const WEBHOOK_EVENTS = [ - 'sandbox.lifecycle.created', - 'sandbox.lifecycle.paused', - 'sandbox.lifecycle.resumed', - 'sandbox.lifecycle.updated', - 'sandbox.lifecycle.killed', -] satisfies SandboxLifecycleEventType[] - -export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number] - export const WEBHOOK_EVENT_LABELS: Record = { 'sandbox.lifecycle.created': 'CREATE', 'sandbox.lifecycle.paused': 'PAUSE', @@ -21,21 +11,4 @@ export const WEBHOOK_EVENT_LABELS: Record = { export const WEBHOOK_DOCS_URL = 'https://e2b.dev/docs/sandbox/lifecycle-events-webhooks' -export const WEBHOOK_EXAMPLE_PAYLOAD = `{ - "id": "", - "version": "v2", - "type": "sandbox.lifecycle.created", - "timestamp": "", - "event_category": "lifecycle", - "event_label": "create", - "sandbox_id": "", - "sandbox_execution_id": "", - "sandbox_template_id": "", - "sandbox_build_id": "", - "sandbox_team_id": "" -} - -// Payload structure may vary by event type. -// See docs for full schema.` - export const WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL = `${WEBHOOK_DOCS_URL}#webhook-verification` From 9bff86020f9a8f7f34a330a09b57329c90d8038d Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 15:10:38 -0400 Subject: [PATCH 47/90] feat(webhooks): enhance webhook detail header and introduce IdBadge component - Refactored WebhookDetailHeader to utilize the new IdBadge component for displaying and copying webhook IDs. - Added functionality to show the latest event timestamp and created date in a more user-friendly format. - Introduced IdBadge component to encapsulate ID display and copy functionality, improving code reusability and readability. - Updated shared index to export the new IdBadge component. These changes aim to improve the user experience in managing webhook details by providing clearer information and streamlined interactions. --- .../settings/webhooks/detail/header.tsx | 92 +++++++++---------- src/features/dashboard/shared/id-badge.tsx | 51 ++++++++++ src/features/dashboard/shared/index.ts | 1 + 3 files changed, 95 insertions(+), 49 deletions(-) create mode 100644 src/features/dashboard/shared/id-badge.tsx diff --git a/src/features/dashboard/settings/webhooks/detail/header.tsx b/src/features/dashboard/settings/webhooks/detail/header.tsx index 2df47aa38..4081f8925 100644 --- a/src/features/dashboard/settings/webhooks/detail/header.tsx +++ b/src/features/dashboard/settings/webhooks/detail/header.tsx @@ -2,12 +2,14 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { WebhookEventBadges } from '@/features/dashboard/settings/webhooks/event-badges' -import { useClipboard } from '@/lib/hooks/use-clipboard' +import { IdBadge } from '@/features/dashboard/shared' import { defaultSuccessToast, toast } from '@/lib/hooks/use-toast' +import { + formatChartTimestampLocal, + formatDate, + formatUTCTimestamp, +} from '@/lib/utils/formatting' import { useTRPC } from '@/trpc/client' -import { Badge } from '@/ui/primitives/badge' -import { Button } from '@/ui/primitives/button' -import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' import { DetailsItem, DetailsRow } from '../../../layouts/details-row' type WebhookDetailHeaderProps = { @@ -15,48 +17,6 @@ type WebhookDetailHeaderProps = { webhookId: string } -const formatDate = (value?: string) => { - if (!value) return '-' - - return new Date(value).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }) -} - -const getWebhookIdBadgeLabel = (id: string) => - `${id.slice(0, 6)}...${id.slice(-6)}` - -const WebhookIdBadge = ({ id }: { id: string }) => { - const [wasCopied, copy] = useClipboard(1500) - - const handleCopy = async () => { - await copy(id) - toast(defaultSuccessToast('Webhook ID copied')) - } - - return ( - - {getWebhookIdBadgeLabel(id)} - - - ) -} - export const WebhookDetailHeader = ({ teamSlug, webhookId, @@ -65,22 +25,56 @@ export const WebhookDetailHeader = ({ const { data } = useSuspenseQuery( trpc.webhooks.get.queryOptions({ teamSlug, webhookId }) ) + const latestDeliveryQuery = useSuspenseQuery( + trpc.webhooks.listDeliveries.queryOptions({ + teamSlug, + webhookId, + limit: 1, + deliveryStatus: 'all', + }) + ) const { webhook } = data + const latestAttempt = + latestDeliveryQuery.data?.groups[0]?.latestAttempt ?? null + const latestEventTimestamp = latestAttempt?.timestamp + const latestEventLabel = latestEventTimestamp + ? `${formatChartTimestampLocal(latestEventTimestamp, true)}, ${formatChartTimestampLocal(latestEventTimestamp)}` + : '-' + const latestEventTitle = latestEventTimestamp + ? formatUTCTimestamp(new Date(latestEventTimestamp)) + : undefined + const handleIdCopied = () => + toast(defaultSuccessToast('Webhook ID copied to clipboard')) return (
- + - -

{formatDate(webhook.createdAt)}

+ +

+ {webhook.url} +

+ +

{formatDate(new Date(webhook.createdAt), 'MMM d, yyyy') ?? '-'}

+
+ +

{latestEventLabel}

+
) diff --git a/src/features/dashboard/shared/id-badge.tsx b/src/features/dashboard/shared/id-badge.tsx new file mode 100644 index 000000000..2685ae179 --- /dev/null +++ b/src/features/dashboard/shared/id-badge.tsx @@ -0,0 +1,51 @@ +'use client' + +import type { MouseEvent } from 'react' +import { useClipboard } from '@/lib/hooks/use-clipboard' +import { Badge } from '@/ui/primitives/badge' +import { Button } from '@/ui/primitives/button' +import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' + +const getIdBadgeLabel = (id: string): string => { + if (id.length <= 8) return id.toUpperCase() + return `${id.slice(0, 4)}...${id.slice(-4)}`.toUpperCase() +} + +interface IdBadgeProps { + id: string + copyAriaLabel?: string + onCopied?: () => void +} + +export const IdBadge = ({ + id, + copyAriaLabel = 'Copy full ID', + onCopied, +}: IdBadgeProps) => { + const [wasCopied, copy] = useClipboard() + const displayId = getIdBadgeLabel(id) + + const handleCopy = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + await copy(id) + onCopied?.() + } + + return ( + + {displayId} + + + ) +} diff --git a/src/features/dashboard/shared/index.ts b/src/features/dashboard/shared/index.ts index acbd7102a..9722c392c 100644 --- a/src/features/dashboard/shared/index.ts +++ b/src/features/dashboard/shared/index.ts @@ -1 +1,2 @@ +export { IdBadge } from './id-badge' export { UserAvatar } from './user-avatar' From 730292bb5cc732a3a7a4b04f710a502b403a8a0e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 15:13:27 -0400 Subject: [PATCH 48/90] Use URL state for webhook stats range --- .../[webhookId]/(tabs)/overview/page.tsx | 17 ++++++++++--- .../webhooks/detail/overview-content.tsx | 25 ++++++++++++++----- .../settings/webhooks/detail/stats-range.ts | 15 +++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx index c68ef5028..f581382c3 100644 --- a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx @@ -1,5 +1,9 @@ import { WebhookOverviewContent } from '@/features/dashboard/settings/webhooks/detail' -import { getWebhookStatsRange } from '@/features/dashboard/settings/webhooks/detail/stats-range' +import { + DEFAULT_WEBHOOK_STATS_RANGE, + getWebhookStatsRange, + loadWebhookStatsRangeParams, +} from '@/features/dashboard/settings/webhooks/detail/stats-range' import { prefetch, trpc } from '@/trpc/server' type WebhookOverviewPageProps = { @@ -7,19 +11,23 @@ type WebhookOverviewPageProps = { teamSlug: string webhookId: string }> + searchParams: Promise> } export default async function WebhookOverviewPage({ params, + searchParams, }: WebhookOverviewPageProps) { const { teamSlug, webhookId } = await params - const range = getWebhookStatsRange('24h') + const { range: rangeParam } = await loadWebhookStatsRangeParams(searchParams) + const range = rangeParam ?? DEFAULT_WEBHOOK_STATS_RANGE + const rangeBounds = getWebhookStatsRange(range) prefetch( trpc.webhooks.getDeliveryStats.queryOptions({ teamSlug, webhookId, - ...range, + ...rangeBounds, }) ) @@ -27,7 +35,8 @@ export default async function WebhookOverviewPage({ ) } diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index b9c697833..4918323c5 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -1,7 +1,8 @@ 'use client' import { useSuspenseQuery } from '@tanstack/react-query' -import { useState } from 'react' +import { useQueryStates } from 'nuqs' +import { useMemo } from 'react' import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts' import { useTRPC } from '@/trpc/client' import { @@ -20,6 +21,7 @@ import { import { WebhookRangeSelector } from './range-selector' import { getWebhookStatsRange, + webhookStatsRangeParams, type WebhookStatsRange, type WebhookStatsRangeBounds, } from './stats-range' @@ -27,6 +29,7 @@ import { type WebhookOverviewContentProps = { teamSlug: string webhookId: string + initialRange: WebhookStatsRange initialRangeBounds: WebhookStatsRangeBounds } @@ -86,11 +89,22 @@ const EmptyChartState = ({ label }: { label: string }) => ( export const WebhookOverviewContent = ({ teamSlug, webhookId, + initialRange, initialRangeBounds, }: WebhookOverviewContentProps) => { - const [range, setRange] = useState('24h') - const [rangeBounds, setRangeBounds] = - useState(initialRangeBounds) + const [rangeParams, setRangeParams] = useQueryStates( + webhookStatsRangeParams, + { + history: 'push', + shallow: true, + } + ) + const range = rangeParams.range ?? initialRange + const rangeBounds = useMemo( + () => + range === initialRange ? initialRangeBounds : getWebhookStatsRange(range), + [range, initialRange, initialRangeBounds] + ) const trpc = useTRPC() const { data } = useSuspenseQuery( trpc.webhooks.getDeliveryStats.queryOptions({ @@ -107,8 +121,7 @@ export const WebhookOverviewContent = ({ : '0%' const hasBuckets = stats.buckets.length > 0 const handleRangeChange = (nextRange: WebhookStatsRange) => { - setRange(nextRange) - setRangeBounds(getWebhookStatsRange(nextRange)) + setRangeParams({ range: nextRange }) } return ( diff --git a/src/features/dashboard/settings/webhooks/detail/stats-range.ts b/src/features/dashboard/settings/webhooks/detail/stats-range.ts index 274aba4e1..97e3639a7 100644 --- a/src/features/dashboard/settings/webhooks/detail/stats-range.ts +++ b/src/features/dashboard/settings/webhooks/detail/stats-range.ts @@ -1,3 +1,7 @@ +import { createLoader, parseAsStringEnum } from 'nuqs/server' + +const WEBHOOK_STATS_RANGE_VALUES = ['24h', '7d'] as const + type WebhookStatsRange = '24h' | '7d' type WebhookStatsRangeBounds = { @@ -5,6 +9,14 @@ type WebhookStatsRangeBounds = { end: string } +const DEFAULT_WEBHOOK_STATS_RANGE: WebhookStatsRange = '24h' + +const webhookStatsRangeParams = { + range: parseAsStringEnum(WEBHOOK_STATS_RANGE_VALUES), +} + +const loadWebhookStatsRangeParams = createLoader(webhookStatsRangeParams) + const WEBHOOK_STATS_RANGE_LABELS: Record = { '24h': 'Last 24 hours', '7d': 'Last 7 days', @@ -31,7 +43,10 @@ const getWebhookStatsRange = ( } export { + DEFAULT_WEBHOOK_STATS_RANGE, getWebhookStatsRange, + loadWebhookStatsRangeParams, + webhookStatsRangeParams, WEBHOOK_STATS_RANGE_LABELS, type WebhookStatsRange, type WebhookStatsRangeBounds, From 8ea57e9b162d5c8629bf0f187349c012305c4daa Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 14 May 2026 15:21:19 -0400 Subject: [PATCH 49/90] refactor(webhooks): simplify webhook name display in settings - Removed the Button wrapper around the webhook name link for a cleaner presentation. - Updated the Link component to include a title attribute for better accessibility and user experience. These changes enhance the clarity and usability of the webhook settings interface. --- .../settings/webhooks/detail/overview-content.tsx | 2 +- .../dashboard/settings/webhooks/table-row.tsx | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index 4918323c5..fb0ad5bb9 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -21,9 +21,9 @@ import { import { WebhookRangeSelector } from './range-selector' import { getWebhookStatsRange, - webhookStatsRangeParams, type WebhookStatsRange, type WebhookStatsRangeBounds, + webhookStatsRangeParams, } from './stats-range' type WebhookOverviewContentProps = { diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index ae895e4c2..c8ef13b0a 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -81,14 +81,13 @@ const WebhookNameAndUrl = ({ href, name, url }: WebhookNameAndUrlProps) => {
- + + {name} + + + + event.preventDefault()} + > + All statuses + + + {WEBHOOK_DELIVERY_STATUSES.map((status) => ( + toggleStatus(status)} + onSelect={(event) => event.preventDefault()} + > + + + ))} + + ) } -const DeliveryDetailSection = ({ - title, - children, +const DeliveryDetailCell = ({ + value, }: { - title: string - children: React.ReactNode -}) => ( -
-

- {title} -

- {children} -
-) + value: string | null | undefined +}) => { + const parsedValue = useMemo(() => parseMaybeJson(value), [value]) -const DeliveryCodeBlock = ({ value }: { value: string | null | undefined }) => ( -
-    {formatMaybeJson(value)}
-  
-) + if (parsedValue === undefined) { + return n/a + } -const DeliveryDetailPanel = ({ - attempt, - group, - isLoading, -}: DeliveryDetailPanelProps) => { - if (!group || !attempt) { + if (typeof parsedValue === 'string') { return ( - - Select an event delivery to inspect the request and response. - + + {parsedValue} + ) } return ( - -
-
-
-

- {getEventLabel(group.eventType)} -

-

- {group.eventId} -

-
- -
-
- HTTP {formatHttpStatus(attempt.httpStatusCode)} - {attempt.durationMs.toLocaleString()}ms - {formatDateTime(attempt.timestamp)} - {group.attemptCount} attempts -
-
- - {isLoading ? ( -

- Loading delivery detail... -

- ) : null} - -
- -
- {group.attempts.map((item) => ( -
-
-

- {formatDateTime(item.timestamp)} -

-

- HTTP {formatHttpStatus(item.httpStatusCode)} ·{' '} - {item.durationMs.toLocaleString()}ms -

-
- -
- ))} -
-
- - -

- {attempt.requestUrl} -

- - -
- - -

- HTTP {formatHttpStatus(attempt.httpStatusCode)} -

- - -
- - {attempt.errorMessage || attempt.errorClass ? ( - -

- {attempt.errorClass || 'delivery_error'} -

-

- {attempt.errorMessage || 'No error message provided'} -

-
- ) : null} -
-
+ + {value} + ) } @@ -246,128 +178,161 @@ export const WebhookDeliveriesContent = ({ teamSlug, webhookId, }: WebhookDeliveriesContentProps) => { - const [deliveryStatus, setDeliveryStatus] = - useState('all') - const [eventType, setEventType] = useState('') - const [selectedEventId, setSelectedEventId] = useState(null) + const [filters, setFilters] = useQueryStates( + { + ...deliveryFilterParams, + ...eventTypeFilterParams, + }, + { shallow: true } + ) const trpc = useTRPC() - const eventTypeFilter = eventType.trim() || undefined + const deliveryStatuses = useMemo( + () => filters.statuses ?? [...WEBHOOK_DELIVERY_STATUSES], + [filters.statuses] + ) + const hasSelectedDeliveryStatuses = deliveryStatuses.length > 0 + const hasAllDeliveryStatuses = + deliveryStatuses.length === WEBHOOK_DELIVERY_STATUSES.length + const deliveryStatusFilter = hasAllDeliveryStatuses + ? undefined + : deliveryStatuses + const handleDeliveryStatusesChange = useCallback( + (nextStatuses: WebhookDeliveryStatus[]) => { + const nextHasAllStatuses = + nextStatuses.length === WEBHOOK_DELIVERY_STATUSES.length + + setFilters({ + statuses: nextHasAllStatuses ? null : nextStatuses, + }) + }, + [setFilters] + ) + const eventTypes = useMemo( + () => filters.types ?? [...SandboxLifecycleEventTypeSchema.options], + [filters.types] + ) + const hasSelectedEventTypes = eventTypes.length > 0 + const hasAllEventTypes = + eventTypes.length === SandboxLifecycleEventTypeSchema.options.length + const eventTypeFilter = hasAllEventTypes ? undefined : eventTypes + const handleEventTypesChange = useCallback( + (nextEventTypes: typeof eventTypes) => { + const nextHasAllEventTypes = + nextEventTypes.length === SandboxLifecycleEventTypeSchema.options.length + + setFilters({ + types: nextHasAllEventTypes ? null : nextEventTypes, + }) + }, + [setFilters] + ) const deliveriesQuery = useInfiniteQuery( trpc.webhooks.listDeliveries.infiniteQueryOptions( { teamSlug, webhookId, limit: 25, - deliveryStatus, + deliveryStatus: deliveryStatusFilter, eventType: eventTypeFilter, }, { + enabled: hasSelectedEventTypes && hasSelectedDeliveryStatuses, getNextPageParam: (page) => page.nextCursor ?? undefined, placeholderData: keepPreviousData, } ) ) const groups = useMemo( - () => deliveriesQuery.data?.pages.flatMap((page) => page.groups) ?? [], - [deliveriesQuery.data] - ) - const selectedGroup = useMemo( () => - groups.find((group) => group.eventId === selectedEventId) ?? groups[0], - [groups, selectedEventId] - ) - const selectedAttempt = selectedGroup?.latestAttempt ?? null - const deliveryDetailQuery = useQuery( - trpc.webhooks.getDelivery.queryOptions( - { - teamSlug, - webhookId, - deliveryId: selectedAttempt?.id ?? EMPTY_UUID, - }, - { - enabled: Boolean(selectedAttempt), - } - ) + hasSelectedEventTypes && hasSelectedDeliveryStatuses + ? (deliveriesQuery.data?.pages.flatMap((page) => page.groups) ?? []) + : [], + [deliveriesQuery.data, hasSelectedDeliveryStatuses, hasSelectedEventTypes] ) - const detailedAttempt = deliveryDetailQuery.data?.delivery ?? selectedAttempt - const hasActiveFilters = deliveryStatus !== 'all' || Boolean(eventTypeFilter) + const hasActiveFilters = !hasAllDeliveryStatuses || !hasAllEventTypes + const isDeliveriesLoading = + hasSelectedEventTypes && + hasSelectedDeliveryStatuses && + deliveriesQuery.isLoading + + const emptyStateLabel = !hasSelectedDeliveryStatuses + ? 'No statuses selected' + : !hasSelectedEventTypes + ? 'No event types selected' + : hasActiveFilters + ? 'No deliveries match these filters' + : 'No deliveries yet' return ( -
+
- { - setDeliveryStatus(value) - setSelectedEventId(null) - }} + - { - setEventType(event.target.value) - setSelectedEventId(null) - }} +
-
+
- +
- + + + + + + + - Event + Event + ID Status - HTTP + Last attempt Attempts Duration - Last attempt + Sandbox ID + Request headers + Request body + Response HTTP + Response headers + Response body - {deliveriesQuery.isLoading ? ( - + {isDeliveriesLoading ? ( + ) : groups.length === 0 ? ( - +

- {hasActiveFilters - ? 'No deliveries match these filters' - : 'No deliveries yet'} + {emptyStateLabel}

) : ( groups.map((group) => { const attempt = group.latestAttempt - const isSelected = group.eventId === selectedGroup?.eventId return ( - setSelectedEventId(group.eventId)} - > - + +
-

- {getEventLabel(group.eventType)} -

-

- {group.sandboxId} -

+
+ + + {attempt ? ( - {attempt - ? formatHttpStatus(attempt.httpStatusCode) - : '-'} + {attempt ? formatDateTime(attempt.timestamp) : '-'} {group.attemptCount} @@ -389,7 +352,24 @@ export const WebhookDeliveriesContent = ({ : '-'} - {attempt ? formatDateTime(attempt.timestamp) : '-'} + + + + + + + + + + {attempt + ? formatHttpStatus(attempt.httpStatusCode) + : '-'} + + + + + +
) @@ -398,18 +378,9 @@ export const WebhookDeliveriesContent = ({
- -
-
-

- Showing {groups.length.toLocaleString()} grouped events -

+
+ + + e.preventDefault()} + > + All events + + + {SandboxLifecycleEventTypeSchema.options.map((type) => ( + toggleType(type)} + onSelect={(e) => e.preventDefault()} + > + + + ))} + + + ) +} diff --git a/src/features/dashboard/shared/event-type-map.ts b/src/features/dashboard/shared/event-type-map.ts new file mode 100644 index 000000000..fb94cc123 --- /dev/null +++ b/src/features/dashboard/shared/event-type-map.ts @@ -0,0 +1,22 @@ +import type { SandboxLifecycleEventType } from '@/core/modules/sandboxes/lifecycle-event-types' +import { + BlockIcon, + CheckIcon, + type Icon, + PausedIcon, + RefreshIcon, + RunningIcon, +} from '@/ui/primitives/icons' + +const SANDBOX_EVENT_TYPE_MAP: Record< + SandboxLifecycleEventType, + { icon: Icon; label: string } +> = { + 'sandbox.lifecycle.created': { icon: CheckIcon, label: 'Created' }, + 'sandbox.lifecycle.updated': { icon: RefreshIcon, label: 'Updated' }, + 'sandbox.lifecycle.paused': { icon: PausedIcon, label: 'Paused' }, + 'sandbox.lifecycle.resumed': { icon: RunningIcon, label: 'Resumed' }, + 'sandbox.lifecycle.killed': { icon: BlockIcon, label: 'Killed' }, +} + +export { SANDBOX_EVENT_TYPE_MAP } diff --git a/src/features/dashboard/shared/index.ts b/src/features/dashboard/shared/index.ts index a18a97d6f..d913954eb 100644 --- a/src/features/dashboard/shared/index.ts +++ b/src/features/dashboard/shared/index.ts @@ -1,3 +1,6 @@ +export { SandboxEventTypeBadge } from './event-type-badge' +export { EventTypeFilter } from './event-type-filter' +export { eventTypeFilterParams } from './event-type-filter-params' export { IdBadge } from './id-badge' export { Timestamp } from './timestamp' export { UserAvatar } from './user-avatar' From f063514d9497b664db8dac98f679d4ac0657ef43 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 18 May 2026 11:30:50 -0400 Subject: [PATCH 55/90] refactor(webhooks): adjust table layout and improve delivery details display - Reduced the minimum width of the deliveries table for better responsiveness. - Updated column widths for improved layout consistency. - Renamed the 'ID' column to 'Sandbox ID' for clarity. - Adjusted loading and empty state handling to reflect the updated column structure. - Enhanced the display of delivery details by utilizing the sandbox ID directly. These changes aim to enhance the usability and clarity of the webhook deliveries content, providing a more streamlined user experience. --- .../webhooks/detail/deliveries-content.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx index ae8257912..63302b6e9 100644 --- a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx @@ -278,15 +278,14 @@ export const WebhookDeliveriesContent = ({
- +
- - - - - + + + + @@ -296,12 +295,11 @@ export const WebhookDeliveriesContent = ({ Event - ID + Sandbox ID Status Last attempt Attempts Duration - Sandbox ID Request headers Request body Response HTTP @@ -311,9 +309,9 @@ export const WebhookDeliveriesContent = ({ {isDeliveriesLoading ? ( - + ) : groups.length === 0 ? ( - +

{emptyStateLabel} @@ -331,7 +329,7 @@ export const WebhookDeliveriesContent = ({ - + {attempt ? ( @@ -342,7 +340,7 @@ export const WebhookDeliveriesContent = ({ '-' )} - + {attempt ? formatDateTime(attempt.timestamp) : '-'} {group.attemptCount} @@ -351,9 +349,6 @@ export const WebhookDeliveriesContent = ({ ? `${attempt.durationMs.toLocaleString()}ms` : '-'} - - - From 190194313a239ac416b1e908aea53f8741f1baa9 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 18 May 2026 12:03:55 -0400 Subject: [PATCH 56/90] refactor(webhooks): enhance delivery data visualization and timestamp handling - Updated the getBucketTimestamp function to accept a timestamp in milliseconds for improved accuracy. - Modified getDeliveryCountSeriesData to include range bounds, ensuring data points align with the specified time range. - Introduced getFailedDeliveryLineData to maintain visibility of line spikes while managing zero values. - Enhanced WebhookStatsChart to support line chart type and improved handling of null values in data points. These changes aim to provide a more accurate and visually coherent representation of webhook delivery data, enhancing user analysis capabilities. --- .../webhooks/detail/overview-content.tsx | 95 ++++++++++++------- .../webhooks/detail/webhook-stats-chart.tsx | 25 ++++- 2 files changed, 83 insertions(+), 37 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index e82457e02..ee492b315 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -117,15 +117,15 @@ const getDeliveryBucketSizeMs = ({ start, end }: WebhookStatsRangeBounds) => { return 60 * 60 * 1000 } -// Buckets an ISO timestamp by duration, e.g. "2026-05-13T14:01:52.123Z" + 1h -> "2026-05-13T14:00:00.000Z". -const getBucketTimestamp = (timestamp: string, bucketSizeMs: number) => { - const time = new Date(timestamp).getTime() - return new Date(Math.floor(time / bucketSizeMs) * bucketSizeMs).toISOString() +// Buckets a timestamp by duration, e.g. 14:01:52 + 1h -> 14:00:00. +const getBucketTimestamp = (timestampMs: number, bucketSizeMs: number) => { + return new Date(Math.floor(timestampMs / bucketSizeMs) * bucketSizeMs) } const getDeliveryCountSeriesData = ( attempts: DeliveryAttempt[], bucketSizeMs: number, + rangeBounds: WebhookStatsRangeBounds, status?: DeliveryAttempt['deliveryStatus'] ) => { const countByBucketTimestamp = new Map() @@ -133,7 +133,10 @@ const getDeliveryCountSeriesData = ( for (const attempt of attempts) { if (status && attempt.deliveryStatus !== status) continue - const timestamp = getBucketTimestamp(attempt.timestamp, bucketSizeMs) + const timestamp = getBucketTimestamp( + new Date(attempt.timestamp).getTime(), + bucketSizeMs + ).toISOString() countByBucketTimestamp.set( timestamp, @@ -141,12 +144,42 @@ const getDeliveryCountSeriesData = ( ) } - return Array.from(countByBucketTimestamp, ([timestamp, value]) => ({ - timestamp, - value, - })) + const bucketStart = getBucketTimestamp(rangeBounds.start, bucketSizeMs) + const points = [] + + for ( + let timestamp = bucketStart.getTime(); + timestamp <= rangeBounds.end; + timestamp += bucketSizeMs + ) { + const bucketTimestamp = new Date(timestamp).toISOString() + points.push({ + timestamp: bucketTimestamp, + value: countByBucketTimestamp.get(bucketTimestamp) ?? 0, + }) + } + + return points } +// Keeps line spikes visible while hiding the zero baseline, e.g. [0, 3, 0, 0] -> [0, 3, 0, null]. +const getFailedDeliveryLineData = ( + points: ReturnType +) => + points.map((point, index) => { + if (point.value > 0) return point + + const previousPoint = points[index - 1] + const nextPoint = points[index + 1] + const isNextToFailure = + (previousPoint?.value ?? 0) > 0 || (nextPoint?.value ?? 0) > 0 + + return { + ...point, + value: isNextToFailure ? 0 : null, + } + }) + export const WebhookOverviewContent = ({ teamSlug, webhookId, @@ -199,38 +232,35 @@ export const WebhookOverviewContent = ({ { name: 'Total deliveries', colorVar: '--accent-info-highlight', - data: getDeliveryCountSeriesData(attempts, deliveryBucketSizeMs), + z: 1, + data: getDeliveryCountSeriesData( + attempts, + deliveryBucketSizeMs, + rangeBounds + ), }, { name: 'Failed deliveries', colorVar: '--accent-error-highlight', - data: getDeliveryCountSeriesData( - attempts, - deliveryBucketSizeMs, - 'failed' + z: 2, + data: getFailedDeliveryLineData( + getDeliveryCountSeriesData( + attempts, + deliveryBucketSizeMs, + rangeBounds, + 'failed' + ) ), }, ] satisfies WebhookStatsChartSeries[] const latencySeries = [ { - name: 'Successful response time', - colorVar: '--accent-positive-highlight', - data: attempts - .filter((attempt) => attempt.deliveryStatus === 'success') - .map((attempt) => ({ - timestamp: attempt.timestamp, - value: attempt.durationMs, - })), - }, - { - name: 'Failed response time', - colorVar: '--accent-error-highlight', - data: attempts - .filter((attempt) => attempt.deliveryStatus === 'failed') - .map((attempt) => ({ - timestamp: attempt.timestamp, - value: attempt.durationMs, - })), + name: 'Response time', + colorVar: '--fg-tertiary', + data: attempts.map((attempt) => ({ + timestamp: attempt.timestamp, + value: attempt.durationMs, + })), }, ] satisfies WebhookStatsChartSeries[] const handleRangeChange = (nextRange: WebhookStatsRange) => { @@ -271,6 +301,7 @@ export const WebhookOverviewContent = ({ {hasAttempts ? ( diff --git a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx index c8d0114cd..f6c9c9e3c 100644 --- a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx +++ b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx @@ -1,7 +1,7 @@ 'use client' import type { EChartsOption, SeriesOption } from 'echarts' -import { ScatterChart } from 'echarts/charts' +import { LineChart, ScatterChart } from 'echarts/charts' import { AxisPointerComponent, GridComponent, @@ -17,6 +17,7 @@ import { cn } from '@/lib/utils' import { calculateAxisMax } from '@/lib/utils/chart' echarts.use([ + LineChart, ScatterChart, GridComponent, TooltipComponent, @@ -26,20 +27,25 @@ echarts.use([ type WebhookStatsChartPoint = { timestamp: string - value: number + value: number | null } type WebhookStatsChartSeries = { name: string data: WebhookStatsChartPoint[] + connectNulls?: boolean + showSymbol?: boolean + z?: number colorVar: | '--accent-info-highlight' | '--accent-error-highlight' | '--accent-positive-highlight' + | '--fg-tertiary' } type WebhookStatsChartProps = { series: WebhookStatsChartSeries[] + chartType?: 'line' | 'scatter' className?: string valueFormatter?: (value: number) => string xAxisMax?: number @@ -67,6 +73,7 @@ const getNumericTooltipValue = (value: unknown) => { const WebhookStatsChart = memo(function WebhookStatsChart({ series, + chartType = 'scatter', className, valueFormatter = defaultValueFormatter, xAxisMax, @@ -77,8 +84,8 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ '--accent-info-highlight', '--accent-error-highlight', '--accent-positive-highlight', - '--stroke', '--fg-tertiary', + '--stroke', '--bg-1', '--font-mono', ] as const) @@ -90,7 +97,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ const option = useMemo(() => { const values = series.flatMap((item) => - item.data.map((point) => point.value) + item.data.flatMap((point) => (point.value === null ? [] : [point.value])) ) const yAxisMax = calculateAxisMax(values.length > 0 ? values : [0], 1.5) @@ -99,16 +106,23 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ return { name: item.name, - type: 'scatter', + type: chartType, + z: item.z, data: item.data.map((point) => [ new Date(point.timestamp).getTime(), point.value, ]), symbol: 'circle', symbolSize: 7, + showSymbol: item.showSymbol ?? chartType === 'scatter', + connectNulls: item.connectNulls, itemStyle: { color, }, + lineStyle: { + color, + width: 2, + }, emphasis: { disabled: true, }, @@ -208,6 +222,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ } }, [ series, + chartType, cssVars, stroke, fgTertiary, From 7ee38c2cf334d0e1e4f991f20a097b5b18b3206c Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 18 May 2026 12:54:09 -0400 Subject: [PATCH 57/90] feat(webhooks): enhance response time metrics and chart visualization - Introduced new ResponseTimeBucketStats type to track response time metrics including count, max, min, and total duration. - Implemented getResponseTimeSeriesData function to calculate response time statistics for delivery attempts, supporting average, minimum, and maximum metrics. - Updated WebhookOverviewContent to display response time data in a line chart format, enhancing visual representation of latency trends. - Enhanced WebhookStatsChart to support synthetic data points and improved tooltip formatting for better user insights. These changes aim to provide a more comprehensive view of webhook response times, facilitating better analysis and monitoring of delivery performance. --- .../webhooks/detail/overview-content.tsx | 132 ++++++++++++++++-- .../webhooks/detail/webhook-stats-chart.tsx | 92 +++++++++--- 2 files changed, 199 insertions(+), 25 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index ee492b315..de7becb6a 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -47,6 +47,13 @@ type ChartCardProps = { type DeliveryAttempt = TRPCRouterOutputs['webhooks']['listDeliveries']['groups'][number]['attempts'][number] +type ResponseTimeBucketStats = { + count: number + maxDurationMs: number + minDurationMs: number + totalDurationMs: number +} + const MetricCard = ({ label, value, description }: MetricCardProps) => ( @@ -71,10 +78,10 @@ const EmptyChartState = ({ label }: { label: string }) => ( const ChartCard = ({ children, title }: ChartCardProps) => (

-
+

{title}

-
{children}
+
{children}
) @@ -180,6 +187,80 @@ const getFailedDeliveryLineData = ( } }) +const getResponseTimeSeriesData = ( + attempts: DeliveryAttempt[], + bucketSizeMs: number, + rangeBounds: WebhookStatsRangeBounds, + metric: 'avg' | 'max' | 'min' +) => { + const statsByBucketTimestamp = new Map() + + for (const attempt of attempts) { + const timestamp = getBucketTimestamp( + new Date(attempt.timestamp).getTime(), + bucketSizeMs + ).toISOString() + const currentStats = statsByBucketTimestamp.get(timestamp) + + statsByBucketTimestamp.set( + timestamp, + currentStats + ? { + count: currentStats.count + 1, + maxDurationMs: Math.max( + currentStats.maxDurationMs, + attempt.durationMs + ), + minDurationMs: Math.min( + currentStats.minDurationMs, + attempt.durationMs + ), + totalDurationMs: currentStats.totalDurationMs + attempt.durationMs, + } + : { + count: 1, + maxDurationMs: attempt.durationMs, + minDurationMs: attempt.durationMs, + totalDurationMs: attempt.durationMs, + } + ) + } + + const bucketStart = getBucketTimestamp(rangeBounds.start, bucketSizeMs) + const points = [] + let hasSeenValue = false + let hasAddedBaseline = false + + for ( + let timestamp = bucketStart.getTime(); + timestamp <= rangeBounds.end; + timestamp += bucketSizeMs + ) { + const bucketTimestamp = new Date(timestamp).toISOString() + const stats = statsByBucketTimestamp.get(bucketTimestamp) + const value = stats + ? metric === 'avg' + ? stats.totalDurationMs / stats.count + : metric === 'max' + ? stats.maxDurationMs + : stats.minDurationMs + : null + + if (value !== null) { + hasSeenValue = true + } + + points.push({ + synthetic: value === null && !hasSeenValue && !hasAddedBaseline, + timestamp: bucketTimestamp, + value: value ?? (!hasSeenValue && !hasAddedBaseline ? 0 : null), + }) + if (!hasSeenValue) hasAddedBaseline = true + } + + return points +} + export const WebhookOverviewContent = ({ teamSlug, webhookId, @@ -255,12 +336,46 @@ export const WebhookOverviewContent = ({ ] satisfies WebhookStatsChartSeries[] const latencySeries = [ { - name: 'Response time', - colorVar: '--fg-tertiary', - data: attempts.map((attempt) => ({ - timestamp: attempt.timestamp, - value: attempt.durationMs, - })), + name: 'Min response time', + colorVar: '--accent-info-highlight', + connectNulls: true, + lineWidth: 2, + showSymbol: true, + z: 1, + data: getResponseTimeSeriesData( + attempts, + deliveryBucketSizeMs, + rangeBounds, + 'min' + ), + }, + { + name: 'Avg response time', + colorVar: '--accent-main-highlight', + connectNulls: true, + lineWidth: 2, + showSymbol: true, + z: 3, + data: getResponseTimeSeriesData( + attempts, + deliveryBucketSizeMs, + rangeBounds, + 'avg' + ), + }, + { + name: 'Max response time', + colorVar: '--accent-warning-highlight', + connectNulls: true, + lineWidth: 2, + showSymbol: true, + z: 2, + data: getResponseTimeSeriesData( + attempts, + deliveryBucketSizeMs, + rangeBounds, + 'max' + ), }, ] satisfies WebhookStatsChartSeries[] const handleRangeChange = (nextRange: WebhookStatsRange) => { @@ -316,6 +431,7 @@ export const WebhookOverviewContent = ({ series={latencySeries} xAxisMin={rangeStartMs} xAxisMax={rangeEndMs} + chartType="line" valueFormatter={(value) => `${value.toLocaleString()}ms`} /> ) : ( diff --git a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx index f6c9c9e3c..d14750aad 100644 --- a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx +++ b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx @@ -26,6 +26,7 @@ echarts.use([ ]) type WebhookStatsChartPoint = { + synthetic?: boolean timestamp: string value: number | null } @@ -34,12 +35,17 @@ type WebhookStatsChartSeries = { name: string data: WebhookStatsChartPoint[] connectNulls?: boolean + lineWidth?: number showSymbol?: boolean z?: number colorVar: + | '--accent-main-highlight' | '--accent-info-highlight' | '--accent-error-highlight' | '--accent-positive-highlight' + | '--accent-warning-highlight' + | '--fg' + | '--fg-secondary' | '--fg-tertiary' } @@ -61,14 +67,29 @@ const formatAxisLabel = (value: number) => const defaultValueFormatter = (value: number) => value.toLocaleString() -const getNumericTooltipValue = (value: unknown) => { - if (typeof value === 'number') return value +const formatTooltipTimestamp = (timestampMs: number) => { + const date = new Date(timestampMs) + const pad = (value: number) => String(value).padStart(2, '0') - if (!Array.isArray(value)) return null + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +} + +const getTooltipTimestampMs = (param: unknown) => { + if (!param || typeof param !== 'object') return null + if (!('value' in param)) return null + if (!Array.isArray(param.value)) return null + + const [timestamp] = param.value + return typeof timestamp === 'number' ? timestamp : null +} - const yValue = value[1] +const getTooltipSyntheticValue = (param: unknown) => { + if (!param || typeof param !== 'object') return false + if (!('data' in param)) return false + if (!param.data || typeof param.data !== 'object') return false + if (!('synthetic' in param.data)) return false - return typeof yValue === 'number' ? yValue : null + return param.data.synthetic === true } const WebhookStatsChart = memo(function WebhookStatsChart({ @@ -81,9 +102,13 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ }: WebhookStatsChartProps) { const { resolvedTheme } = useTheme() const cssVars = useCssVars([ + '--accent-main-highlight', '--accent-info-highlight', '--accent-error-highlight', '--accent-positive-highlight', + '--accent-warning-highlight', + '--fg', + '--fg-secondary', '--fg-tertiary', '--stroke', '--bg-1', @@ -101,6 +126,42 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ ) const yAxisMax = calculateAxisMax(values.length > 0 ? values : [0], 1.5) + const getTooltipContent = (param: unknown) => { + if (getTooltipSyntheticValue(param)) return '' + + const timestampMs = getTooltipTimestampMs(param) + if (timestampMs === null) return '' + + const rows = series.flatMap((item) => { + const point = item.data.find( + (point) => + !point.synthetic && + point.value !== null && + new Date(point.timestamp).getTime() === timestampMs + ) + if (!point || point.value === null) return [] + + const color = cssVars[item.colorVar] || '#000' + + return [ + `
+ + + ${item.name} + + ${valueFormatter(point.value)} +
`, + ] + }) + + if (rows.length === 0) return '' + + return `
+
${formatTooltipTimestamp(timestampMs)}
+ ${rows.join('')} +
` + } + const chartSeries: SeriesOption[] = series.map((item) => { const color = cssVars[item.colorVar] || '#000' @@ -108,12 +169,13 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ name: item.name, type: chartType, z: item.z, - data: item.data.map((point) => [ - new Date(point.timestamp).getTime(), - point.value, - ]), + data: item.data.map((point) => ({ + synthetic: point.synthetic, + value: [new Date(point.timestamp).getTime(), point.value], + })), symbol: 'circle', - symbolSize: 7, + symbolSize: (_value: unknown, params: unknown) => + getTooltipSyntheticValue(params) ? 0 : 7, showSymbol: item.showSymbol ?? chartType === 'scatter', connectNulls: item.connectNulls, itemStyle: { @@ -121,7 +183,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ }, lineStyle: { color, - width: 2, + width: item.lineWidth ?? 2, }, emphasis: { disabled: true, @@ -139,7 +201,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ left: 42, }, tooltip: { - trigger: 'axis', + trigger: 'item', confine: true, backgroundColor: bg, borderColor: stroke, @@ -160,11 +222,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ show: false, }, }, - valueFormatter: (value) => { - const numericValue = getNumericTooltipValue(value) - - return numericValue === null ? '' : valueFormatter(numericValue) - }, + formatter: getTooltipContent, }, xAxis: { type: 'time', From 6916abf449e63a844c607bbef9cff9f7f7c95e93 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 18 May 2026 12:55:03 -0400 Subject: [PATCH 58/90] fix(webhooks): improve tooltip transition for webhook stats chart - Added transitionDuration property to the tooltip configuration in WebhookStatsChart, enhancing the visual experience during data interactions. This change aims to provide a smoother user experience when interacting with the webhook statistics chart. --- .../dashboard/settings/webhooks/detail/webhook-stats-chart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx index d14750aad..20ecf5d2a 100644 --- a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx +++ b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx @@ -203,6 +203,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ tooltip: { trigger: 'item', confine: true, + transitionDuration: 0, backgroundColor: bg, borderColor: stroke, borderWidth: 1, From 1831ec5e9a549aa1ece3b006ffcee8a94972538b Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 19 May 2026 13:01:00 -0400 Subject: [PATCH 59/90] refactor(webhooks): simplify webhook name display and enhance row interaction - Removed the Link component from the WebhookNameAndUrl, replacing it with a simple paragraph for cleaner presentation. - Updated the WebhookTableRow to use the useRouter hook for navigation, allowing row clicks to redirect to the webhook details. - Adjusted the WebhookNameAndUrlProps to eliminate the href prop, streamlining the component's interface. These changes aim to improve the user experience by simplifying the display and enhancing interaction with webhook rows. --- .../dashboard/settings/webhooks/table-row.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index c8ef13b0a..57bd97526 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import { useRouter } from 'next/navigation' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' import { useClipboard } from '@/lib/hooks/use-clipboard' @@ -42,7 +42,6 @@ type WebhookRowActionsProps = { } type WebhookNameAndUrlProps = { - href: string name: string url: string } @@ -55,7 +54,7 @@ const urlIconMap: Record = { idle: WebhookIcon, } -const WebhookNameAndUrl = ({ href, name, url }: WebhookNameAndUrlProps) => { +const WebhookNameAndUrl = ({ name, url }: WebhookNameAndUrlProps) => { const [wasCopied, copy] = useClipboard(1500) const [isUrlHovered, setIsUrlHovered] = useState(false) const iconState: UrlIconState = wasCopied @@ -81,13 +80,12 @@ const WebhookNameAndUrl = ({ href, name, url }: WebhookNameAndUrlProps) => {
- {name} - +

+
From 21b766c72399730726bdd61476ba1b072767382c Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 19 May 2026 13:11:09 -0400 Subject: [PATCH 61/90] refactor(webhooks): update table and cell widths for improved layout --- src/features/dashboard/settings/webhooks/table-row.tsx | 6 +++--- src/features/dashboard/settings/webhooks/table.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 57bd97526..a9c418d03 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -93,7 +93,7 @@ const WebhookNameAndUrl = ({ name, url }: WebhookNameAndUrlProps) => { onMouseEnter={() => setIsUrlHovered(true)} onMouseLeave={() => setIsUrlHovered(false)} aria-label={`Copy webhook URL ${url}`} - className="w-full min-w-0 justify-start font-mono uppercase prose-label-numeric" + className="w-fit max-w-full min-w-0 justify-start font-mono uppercase prose-label-numeric" > {url} @@ -172,9 +172,9 @@ export const WebhookTableRow = ({ webhook }: WebhookRowProps) => { - +
-

+

{createdAt}

diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index b4e5e77ee..ebae007ed 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -34,11 +34,16 @@ export const WebhooksTable = ({ : 'No webhooks match your search' return ( -
+
- + From 63f898cd6031aeb9db4573c89701821ea0fd636b Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 19 May 2026 13:15:33 -0400 Subject: [PATCH 62/90] refactor(webhooks): updated the SelectTrigger component in the WebhookRangeSelector to include a solid border and font styling, enhancing the overall appearance and consistency of the user interface. --- .../dashboard/settings/webhooks/detail/range-selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx index b22683afd..681b9a8b4 100644 --- a/src/features/dashboard/settings/webhooks/detail/range-selector.tsx +++ b/src/features/dashboard/settings/webhooks/detail/range-selector.tsx @@ -30,7 +30,7 @@ export const WebhookRangeSelector = ({ return (
- - - - - - - - - - - - - - - - Event - Sandbox ID - Status - Last attempt - Attempts - Duration - Request headers - Request body - Response HTTP - Response headers - Response body - - - - {isDeliveriesLoading ? ( - - ) : groups.length === 0 ? ( - - -

- {emptyStateLabel} -

-
- ) : ( - groups.map((group) => { - const attempt = group.latestAttempt +
+
+ + + + + + + + + + + + + + + + Event + Sandbox ID + Status + Last attempt + Attempts + Duration + Request headers + Request body + Response HTTP + Response headers + Response body + + + + {isDeliveriesLoading ? ( + + ) : groups.length === 0 ? ( + + +

+ {emptyStateLabel} +

+
+ ) : ( + groups.map((group) => { + const attempt = group.latestAttempt - return ( - - -
- -
-
- - - - - {attempt ? ( - - ) : ( - '-' - )} - - - {attempt ? formatDateTime(attempt.timestamp) : '-'} - - {group.attemptCount} - - {attempt - ? `${attempt.durationMs.toLocaleString()}ms` - : '-'} - - - - - - - - - {attempt - ? formatHttpStatus(attempt.httpStatusCode) - : '-'} - - - - - - - -
- ) - }) - )} -
-
-
+ return ( + + +
+ +
+
+ + + toast(defaultSuccessToast('Sandbox ID copied')) + } + /> + + + {attempt ? ( + + ) : ( + '-' + )} + + + {attempt ? formatDateTime(attempt.timestamp) : '-'} + + {group.attemptCount} + + {attempt + ? `${attempt.durationMs.toLocaleString()}ms` + : '-'} + + + + + + + + + {attempt ? formatHttpStatus(attempt.httpStatusCode) : '-'} + + + + + + + +
+ ) + }) + )} + +
-
+
-
+
+
) From f67d03525eb47b56a0615fe6c0011c81c833ec11 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 19 May 2026 17:10:22 -0400 Subject: [PATCH 71/90] refactor(webhooks): enhance layout and responsiveness of webhook overview components - Updated ChartPanel and WebhookStatsChart to improve layout and responsiveness, ensuring better visual structure. - Adjusted CSS classes for proper overflow handling and minimum height settings, enhancing the overall user experience. These changes aim to streamline the presentation of webhook statistics and improve the usability of the overview content. --- .../dashboard/settings/webhooks/detail/overview-content.tsx | 4 ++-- .../settings/webhooks/detail/webhook-stats-chart.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index e3bbd54bd..50d2a947c 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -65,7 +65,7 @@ const MetricPanel = ({ label, value, description }: MetricPanelProps) => ( ) const ChartPanel = ({ children, title }: ChartPanelProps) => ( -
+
{title} @@ -463,7 +463,7 @@ export const WebhookOverviewContent = ({ />
-
+
+
Date: Tue, 19 May 2026 17:13:11 -0400 Subject: [PATCH 72/90] style: adjust minimum width of webhooks table for better layout --- src/features/dashboard/settings/webhooks/table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/settings/webhooks/table.tsx b/src/features/dashboard/settings/webhooks/table.tsx index b4e5e77ee..dd34cb992 100644 --- a/src/features/dashboard/settings/webhooks/table.tsx +++ b/src/features/dashboard/settings/webhooks/table.tsx @@ -34,7 +34,7 @@ export const WebhooksTable = ({ : 'No webhooks match your search' return ( - +
From ea2a47ca0bba74a653ad0e68a66494e28d8ea02e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 19 May 2026 17:25:13 -0400 Subject: [PATCH 73/90] style: update layout of deliveries content for improved alignment - Changed the layout of the deliveries content to use a flex row for better alignment of items. - Adjusted CSS classes to enhance the overall visual structure and responsiveness of the component. These changes aim to streamline the presentation of webhook delivery details, improving user experience. --- .../dashboard/settings/webhooks/detail/deliveries-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx index 230ab5d0a..fec0c06e5 100644 --- a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx @@ -547,7 +547,7 @@ export const WebhookDeliveriesContent = ({ return (
-
+
Date: Wed, 20 May 2026 10:54:50 -0400 Subject: [PATCH 74/90] refactor(webhooks): update webhook overview content for improved clarity and formatting - Adjusted z-index values for delivery metrics to enhance visual hierarchy. - Renamed response time labels for brevity and clarity. - Improved value formatting for response times to include decimal precision and rounded display for y-axis values. These changes aim to streamline the presentation of webhook statistics, enhancing user experience and data readability. --- .../webhooks/detail/overview-content.tsx | 20 +++++++++---- .../webhooks/detail/webhook-stats-chart.tsx | 28 +++++++++++++++---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index 50d2a947c..c8ac797bd 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -379,7 +379,7 @@ export const WebhookOverviewContent = ({ name: 'Total deliveries', colorVar: '--accent-info-highlight', showSymbol: true, - z: 1, + z: 2, data: attempts.length > 0 ? getDeliveryCountSeriesData(attempts, rangeBounds, grouping) @@ -389,7 +389,7 @@ export const WebhookOverviewContent = ({ name: 'Failed deliveries', colorVar: '--accent-error-highlight', showSymbol: true, - z: 2, + z: 1, data: attempts.length > 0 ? getDeliveryCountSeriesData( @@ -403,7 +403,7 @@ export const WebhookOverviewContent = ({ ] satisfies WebhookStatsChartSeries[] const latencySeries = [ { - name: 'Min response time', + name: 'Min', colorVar: '--accent-info-highlight', connectNulls: true, lineWidth: 2, @@ -412,7 +412,7 @@ export const WebhookOverviewContent = ({ data: getResponseTimeSeriesData(attempts, rangeBounds, grouping, 'min'), }, { - name: 'Avg response time', + name: 'Average', colorVar: '--accent-main-highlight', connectNulls: true, lineWidth: 2, @@ -421,7 +421,7 @@ export const WebhookOverviewContent = ({ data: getResponseTimeSeriesData(attempts, rangeBounds, grouping, 'avg'), }, { - name: 'Max response time', + name: 'Max', colorVar: '--accent-warning-highlight', connectNulls: true, lineWidth: 2, @@ -481,7 +481,15 @@ export const WebhookOverviewContent = ({ xAxisMax={rangeEndMs} xAxisScale={xAxisScale} chartType="line" - valueFormatter={(value) => `${value.toLocaleString()}ms`} + valueFormatter={(value) => + `${value.toLocaleString('en-US', { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + })}ms` + } + yAxisValueFormatter={(value) => + `${Math.round(value).toLocaleString()}ms` + } />
diff --git a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx index 13675c20a..c1ab479cb 100644 --- a/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx +++ b/src/features/dashboard/settings/webhooks/detail/webhook-stats-chart.tsx @@ -55,6 +55,7 @@ type WebhookStatsChartProps = { chartType?: 'line' | 'scatter' className?: string valueFormatter?: (value: number) => string + yAxisValueFormatter?: (value: number) => string xAxisScale?: 'daily' | 'four-hour' | 'twelve-hour' | 'today' xAxisMax?: number xAxisMin?: number @@ -62,6 +63,8 @@ type WebhookStatsChartProps = { const HOUR_MS = 60 * 60 * 1000 const DAY_MS = 24 * HOUR_MS +const AXIS_LABEL_GRID_GAP = 8 +const MONO_AXIS_LABEL_CHAR_WIDTH = 7.2 const formatAxisLabel = ( value: number, @@ -152,6 +155,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ chartType = 'scatter', className, valueFormatter = defaultValueFormatter, + yAxisValueFormatter = valueFormatter, xAxisScale = 'daily', xAxisMax, xAxisMin, @@ -182,6 +186,12 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ item.data.flatMap((point) => (point.value === null ? [] : [point.value])) ) const yAxisMax = calculateAxisMax(values.length > 0 ? values : [0], 1.5) + const yAxisLabels = [0, yAxisMax / 2, yAxisMax].map(yAxisValueFormatter) + const yAxisLabelGutter = + Math.ceil( + Math.max(...yAxisLabels.map((label) => label.length)) * + MONO_AXIS_LABEL_CHAR_WIDTH + ) + AXIS_LABEL_GRID_GAP const xAxisInterval = getXAxisInterval({ scale: xAxisScale, xAxisMax, @@ -206,21 +216,23 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ const color = cssVars[item.colorVar] || '#000' return [ - `
- + `
+ + ${item.name} + - ${valueFormatter(point.value)} + ${valueFormatter(point.value)}
`, ] }) if (rows.length === 0) return '' - return `
+ return `
${formatTooltipTimestamp(timestampMs, xAxisScale)}
- ${rows.join('')} +
${rows.join('')}
` } @@ -260,7 +272,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ top: 16, right: 16, bottom: 28, - left: 42, + left: yAxisLabelGutter, }, tooltip: { trigger: 'item', @@ -326,10 +338,13 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ axisLine: { show: false }, axisTick: { show: false }, axisLabel: { + align: 'left', color: fgTertiary, fontFamily: fontMono, fontSize: 12, interval: 0, + margin: yAxisLabelGutter, + formatter: (value: number) => yAxisValueFormatter(value), }, splitLine: { show: true, @@ -352,6 +367,7 @@ const WebhookStatsChart = memo(function WebhookStatsChart({ bg, fontMono, valueFormatter, + yAxisValueFormatter, xAxisScale, xAxisMax, xAxisMin, From 13026c2af5ded1d894bf85b35b1c5008d931a2a4 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 20 May 2026 11:17:10 -0400 Subject: [PATCH 75/90] refactor(webhooks): rename 'Average' to 'Avg' in webhook overview content for brevity --- .../dashboard/settings/webhooks/detail/overview-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index c8ac797bd..70acf7ecc 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -412,7 +412,7 @@ export const WebhookOverviewContent = ({ data: getResponseTimeSeriesData(attempts, rangeBounds, grouping, 'min'), }, { - name: 'Average', + name: 'Avg', colorVar: '--accent-main-highlight', connectNulls: true, lineWidth: 2, From d2b7debdea7e4b248b06f1f041a066fb4b16d0c3 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 20 May 2026 11:25:58 -0400 Subject: [PATCH 76/90] refactor(webhooks): update OpenAPI specification and TypeScript types for webhook delivery details - Rearranged properties in the OpenAPI specification for better organization. - Updated TypeScript types to reflect changes in the OpenAPI schema, including renaming and reordering of fields. - Adjusted the handling of HTTP status codes in the deliveries content to align with the updated API structure. These changes aim to enhance the clarity and consistency of webhook delivery data representation. --- spec/openapi.argus.yaml | 30 +++++++++---------- src/core/shared/contracts/argus-api.types.ts | 22 +++++++------- .../webhooks/detail/deliveries-content.tsx | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index f3c93b01e..aa1ceacc8 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -284,9 +284,9 @@ components: - eventType - deliveryStatus - durationMs - - requestUrl - - requestHeaders - requestBody + - requestHeaders + - requestUrl - errorClass - timestamp properties: @@ -316,33 +316,33 @@ components: type: string enum: [success, failed] description: Delivery attempt status - httpStatusCode: - type: integer - format: int32 - nullable: true - description: HTTP response status code, if a response was received durationMs: type: integer format: int32 description: Delivery request duration in milliseconds - requestUrl: + requestBody: type: string - format: uri - description: URL attempted for this delivery + description: Serialized webhook request body requestHeaders: type: string description: JSON-encoded request headers with sensitive values redacted - requestBody: + requestUrl: type: string - description: Serialized webhook request body + format: uri + description: URL attempted for this delivery + responseBody: + type: string + nullable: true + description: Truncated response body, if a response was received responseHeaders: type: string nullable: true description: JSON-encoded response headers, if a response was received - responseBody: - type: string + responseHttpStatusCode: + type: integer + format: int32 nullable: true - description: Truncated response body, if a response was received + description: HTTP response status code, if a response was received errorClass: type: string description: Machine-readable non-HTTP or HTTP failure class diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index 75a939e8b..b4386f5af 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -371,29 +371,29 @@ export interface components { * @enum {string} */ deliveryStatus: 'success' | 'failed' - /** - * Format: int32 - * @description HTTP response status code, if a response was received - */ - httpStatusCode?: number | null /** * Format: int32 * @description Delivery request duration in milliseconds */ durationMs: number + /** @description Serialized webhook request body */ + requestBody: string + /** @description JSON-encoded request headers with sensitive values redacted */ + requestHeaders: string /** * Format: uri * @description URL attempted for this delivery */ requestUrl: string - /** @description JSON-encoded request headers with sensitive values redacted */ - requestHeaders: string - /** @description Serialized webhook request body */ - requestBody: string - /** @description JSON-encoded response headers, if a response was received */ - responseHeaders?: string | null /** @description Truncated response body, if a response was received */ responseBody?: string | null + /** @description JSON-encoded response headers, if a response was received */ + responseHeaders?: string | null + /** + * Format: int32 + * @description HTTP response status code, if a response was received + */ + responseHttpStatusCode?: number | null /** @description Machine-readable non-HTTP or HTTP failure class */ errorClass: string /** @description Error message for failures without a useful response body */ diff --git a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx index fec0c06e5..e990b0e63 100644 --- a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx @@ -392,7 +392,7 @@ const WebhookDeliveryRow = ({ - {attempt ? formatHttpStatus(attempt.httpStatusCode) : '-'} + {attempt ? formatHttpStatus(attempt.responseHttpStatusCode) : '-'} From 9e96afc543e94415d7aea22c8bea0cfddb1db900 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 20 May 2026 15:58:17 -0400 Subject: [PATCH 77/90] refactor(webhooks): standardize webhook delivery status and duration properties - Renamed 'deliveryStatus' to 'status' in the OpenAPI specification and TypeScript types for consistency. - Updated duration properties to use a unified structure, replacing individual min, avg, and max fields with a single 'durationMs' reference. - Adjusted related components and queries to align with the new API structure, enhancing clarity and maintainability. These changes aim to improve the consistency and readability of webhook delivery data representation. --- spec/openapi.argus.yaml | 31 +++-- .../[webhookId]/(tabs)/overview/page.tsx | 4 +- src/core/shared/contracts/argus-api.types.ts | 15 ++- .../webhooks/detail/deliveries-content.tsx | 6 +- .../webhooks/detail/overview-content.tsx | 114 +++++++----------- 5 files changed, 71 insertions(+), 99 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index aa1ceacc8..bbfbb7055 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -282,7 +282,7 @@ components: - eventId - sandboxId - eventType - - deliveryStatus + - status - durationMs - requestBody - requestHeaders @@ -312,7 +312,7 @@ components: eventType: type: string description: Sandbox event type - deliveryStatus: + status: type: string enum: [success, failed] description: Delivery attempt status @@ -361,9 +361,7 @@ components: - buckets - total - failed - - minDurationMs - - avgDurationMs - - maxDurationMs + - durationMs properties: buckets: type: array @@ -375,13 +373,23 @@ components: failed: type: integer format: int64 - minDurationMs: + durationMs: + $ref: "#/components/schemas/WebhookDeliveryDurationStats" + + WebhookDeliveryDurationStats: + description: Webhook delivery duration statistics in milliseconds + required: + - minimum + - average + - maximum + properties: + minimum: type: integer format: int32 - avgDurationMs: + average: type: number format: double - maxDurationMs: + maximum: type: integer format: int32 @@ -391,7 +399,7 @@ components: - timestamp - total - failed - - avgDurationMs + - durationMs properties: timestamp: type: string @@ -402,9 +410,8 @@ components: failed: type: integer format: int64 - avgDurationMs: - type: number - format: double + durationMs: + $ref: "#/components/schemas/WebhookDeliveryDurationStats" WebhookDeliveryEvent: description: Webhook delivery attempts grouped by sandbox event diff --git a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx index 704df116a..2be576401 100644 --- a/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx +++ b/src/app/dashboard/[teamSlug]/webhooks/[webhookId]/(tabs)/overview/page.tsx @@ -29,11 +29,9 @@ export default async function WebhookOverviewPage({ const apiRangeBounds = getWebhookStatsApiBounds(rangeBounds) prefetch( - trpc.webhooks.listDeliveries.queryOptions({ + trpc.webhooks.getDeliveryStats.queryOptions({ teamSlug, webhookId, - limit: 100, - orderAsc: true, ...apiRangeBounds, }) ) diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index b4386f5af..7beba8bce 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -370,7 +370,7 @@ export interface components { * @description Delivery attempt status * @enum {string} */ - deliveryStatus: 'success' | 'failed' + status: 'success' | 'failed' /** * Format: int32 * @description Delivery request duration in milliseconds @@ -411,12 +411,16 @@ export interface components { total: number /** Format: int64 */ failed: number + durationMs: components['schemas']['WebhookDeliveryDurationStats'] + } + /** @description Webhook delivery duration statistics in milliseconds */ + WebhookDeliveryDurationStats: { /** Format: int32 */ - minDurationMs: number + minimum: number /** Format: double */ - avgDurationMs: number + average: number /** Format: int32 */ - maxDurationMs: number + maximum: number } /** @description Webhook delivery stats for a time bucket */ WebhookDeliveryStatsBucket: { @@ -426,8 +430,7 @@ export interface components { total: number /** Format: int64 */ failed: number - /** Format: double */ - avgDurationMs: number + durationMs: components['schemas']['WebhookDeliveryDurationStats'] } /** @description Webhook delivery attempts grouped by sandbox event */ WebhookDeliveryEvent: { diff --git a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx index e990b0e63..8e6e4ba31 100644 --- a/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/deliveries-content.tsx @@ -370,11 +370,7 @@ const WebhookDeliveryRow = ({ /> - {attempt ? ( - - ) : ( - '-' - )} + {attempt ? : '-'} {attempt ? formatDateTime(attempt.timestamp) : '-'} diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index 70acf7ecc..e8619890e 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -38,8 +38,8 @@ type ChartPanelProps = { title: string } -type DeliveryAttempt = - TRPCRouterOutputs['webhooks']['listDeliveries']['groups'][number]['attempts'][number] +type WebhookDeliveryStats = + TRPCRouterOutputs['webhooks']['getDeliveryStats']['stats'] type ResponseTimeTimestampStats = { count: number @@ -50,6 +50,8 @@ type ResponseTimeTimestampStats = { } type WebhookStatsGrouping = 'day' | 'timestamp' +type WebhookDeliveryStatsBucket = WebhookDeliveryStats['buckets'][number] +type WebhookDeliveryStatus = 'failed' const DAY_MS = 24 * 60 * 60 * 1000 const MINUTE_MS = 60 * 1000 @@ -75,34 +77,6 @@ const ChartPanel = ({ children, title }: ChartPanelProps) => ( ) -const getAttemptsFromGroups = ( - groups: TRPCRouterOutputs['webhooks']['listDeliveries']['groups'] -) => - groups - .flatMap((group) => group.attempts) - .sort( - (left, right) => - new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime() - ) - -const getAttemptStats = (attempts: DeliveryAttempt[]) => { - const total = attempts.length - const failed = attempts.filter( - (attempt) => attempt.deliveryStatus === 'failed' - ).length - const durations = attempts.map((attempt) => attempt.durationMs) - const durationTotal = durations.reduce((sum, value) => sum + value, 0) - - return { - total, - failed, - successful: total - failed, - minDurationMs: durations.length > 0 ? Math.min(...durations) : 0, - avgDurationMs: durations.length > 0 ? durationTotal / durations.length : 0, - maxDurationMs: durations.length > 0 ? Math.max(...durations) : 0, - } -} - const getStartOfDay = (timestampMs: number) => { const date = new Date(timestampMs) date.setHours(0, 0, 0, 0) @@ -120,22 +94,23 @@ const getSeriesTimestamp = ( return timestampMs } -// Groups delivery attempts by chart granularity, e.g. retries at "14:35:10" -> one "14:35" count. +// Groups delivery buckets by chart granularity, e.g. minute buckets from one day -> one daily count. const getDeliveryCountSeriesData = ( - attempts: DeliveryAttempt[], + buckets: WebhookDeliveryStatsBucket[], rangeBounds: WebhookStatsRangeBounds, grouping: WebhookStatsGrouping, - status?: DeliveryAttempt['deliveryStatus'] + status?: WebhookDeliveryStatus ) => { const countByTimestamp = new Map< number, { count: number; timestampMs: number } >() - for (const attempt of attempts) { - if (status && attempt.deliveryStatus !== status) continue + for (const bucket of buckets) { + const count = status === 'failed' ? bucket.failed : bucket.total + if (count <= 0) continue - const timestampMs = getSeriesTimestamp(attempt.timestamp, grouping) + const timestampMs = getSeriesTimestamp(bucket.timestamp, grouping) const bucketTimestampMs = grouping === 'day' ? timestampMs @@ -143,7 +118,7 @@ const getDeliveryCountSeriesData = ( const current = countByTimestamp.get(bucketTimestampMs) countByTimestamp.set(bucketTimestampMs, { - count: (current?.count ?? 0) + 1, + count: (current?.count ?? 0) + count, timestampMs: Math.max(current?.timestampMs ?? timestampMs, timestampMs), }) } @@ -245,45 +220,48 @@ const getEmptyDeliveryCountSeriesData = ( ] } -// Groups response times by chart granularity, e.g. retries at "14:35:10" -> one "14:35" min/avg/max point. +// Groups response-time buckets by chart granularity, e.g. minute buckets from one day -> one daily min/avg/max point. const getResponseTimeSeriesData = ( - attempts: DeliveryAttempt[], + buckets: WebhookDeliveryStatsBucket[], rangeBounds: WebhookStatsRangeBounds, grouping: WebhookStatsGrouping, metric: 'avg' | 'max' | 'min' ) => { const statsByTimestamp = new Map() - for (const attempt of attempts) { - const timestampMs = getSeriesTimestamp(attempt.timestamp, grouping) + for (const bucket of buckets) { + if (bucket.total <= 0) continue + + const timestampMs = getSeriesTimestamp(bucket.timestamp, grouping) const bucketTimestampMs = grouping === 'day' ? timestampMs : Math.floor(timestampMs / MINUTE_MS) * MINUTE_MS const currentStats = statsByTimestamp.get(bucketTimestampMs) + const durationTotal = bucket.durationMs.average * bucket.total statsByTimestamp.set( bucketTimestampMs, currentStats ? { - count: currentStats.count + 1, + count: currentStats.count + bucket.total, maxDurationMs: Math.max( currentStats.maxDurationMs, - attempt.durationMs + bucket.durationMs.maximum ), minDurationMs: Math.min( currentStats.minDurationMs, - attempt.durationMs + bucket.durationMs.minimum ), timestampMs: Math.max(currentStats.timestampMs, timestampMs), - totalDurationMs: currentStats.totalDurationMs + attempt.durationMs, + totalDurationMs: currentStats.totalDurationMs + durationTotal, } : { - count: 1, - maxDurationMs: attempt.durationMs, - minDurationMs: attempt.durationMs, + count: bucket.total, + maxDurationMs: bucket.durationMs.maximum, + minDurationMs: bucket.durationMs.minimum, timestampMs, - totalDurationMs: attempt.durationMs, + totalDurationMs: durationTotal, } ) } @@ -345,19 +323,14 @@ export const WebhookOverviewContent = ({ const range = getWebhookStatsRangeFromBounds(rangeBounds) const trpc = useTRPC() const { data } = useSuspenseQuery( - trpc.webhooks.listDeliveries.queryOptions({ + trpc.webhooks.getDeliveryStats.queryOptions({ teamSlug, webhookId, - limit: 100, - orderAsc: true, ...apiRangeBounds, }) ) - const attempts = useMemo( - () => getAttemptsFromGroups(data.groups), - [data.groups] - ) - const stats = getAttemptStats(attempts) + const stats = data.stats + const buckets = stats.buckets const failureRate = stats.total > 0 ? `${((stats.failed / stats.total) * 100).toFixed(1)}%` @@ -381,8 +354,8 @@ export const WebhookOverviewContent = ({ showSymbol: true, z: 2, data: - attempts.length > 0 - ? getDeliveryCountSeriesData(attempts, rangeBounds, grouping) + buckets.length > 0 + ? getDeliveryCountSeriesData(buckets, rangeBounds, grouping) : getEmptyDeliveryCountSeriesData(rangeBounds, grouping), }, { @@ -391,13 +364,8 @@ export const WebhookOverviewContent = ({ showSymbol: true, z: 1, data: - attempts.length > 0 - ? getDeliveryCountSeriesData( - attempts, - rangeBounds, - grouping, - 'failed' - ) + buckets.length > 0 + ? getDeliveryCountSeriesData(buckets, rangeBounds, grouping, 'failed') : [], }, ] satisfies WebhookStatsChartSeries[] @@ -409,7 +377,7 @@ export const WebhookOverviewContent = ({ lineWidth: 2, showSymbol: true, z: 1, - data: getResponseTimeSeriesData(attempts, rangeBounds, grouping, 'min'), + data: getResponseTimeSeriesData(buckets, rangeBounds, grouping, 'min'), }, { name: 'Avg', @@ -418,7 +386,7 @@ export const WebhookOverviewContent = ({ lineWidth: 2, showSymbol: true, z: 3, - data: getResponseTimeSeriesData(attempts, rangeBounds, grouping, 'avg'), + data: getResponseTimeSeriesData(buckets, rangeBounds, grouping, 'avg'), }, { name: 'Max', @@ -427,7 +395,7 @@ export const WebhookOverviewContent = ({ lineWidth: 2, showSymbol: true, z: 2, - data: getResponseTimeSeriesData(attempts, rangeBounds, grouping, 'max'), + data: getResponseTimeSeriesData(buckets, rangeBounds, grouping, 'max'), }, ] satisfies WebhookStatsChartSeries[] const handleRangeChange = (nextRange: WebhookStatsRange) => { @@ -444,7 +412,7 @@ export const WebhookOverviewContent = ({
From 9b9273becee2a4601dc2cd25f1eac15f252cd33d Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 20 May 2026 16:02:46 -0400 Subject: [PATCH 78/90] refactor(webhooks): enhance webhook delivery data visualization - Introduced a function to hide inactive zero-value points in the delivery chart, improving clarity by reducing clutter. - Adjusted z-index for failed deliveries based on the presence of failed delivery data, enhancing visual hierarchy. - Updated the delivery series data to incorporate the new function, ensuring a cleaner representation of webhook statistics. These changes aim to improve the readability and user experience of webhook delivery metrics. --- .../webhooks/detail/overview-content.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index e8619890e..079d9201b 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -220,6 +220,17 @@ const getEmptyDeliveryCountSeriesData = ( ] } +const hideInactiveZeroValuePoints = (points: WebhookStatsChartPoint[]) => + points.map((point, index) => { + if (point.value !== 0) return point + + const hasAdjacentValue = + (points[index - 1]?.value ?? 0) > 0 || (points[index + 1]?.value ?? 0) > 0 + if (hasAdjacentValue) return point + + return { ...point, synthetic: true, value: null } + }) + // Groups response-time buckets by chart granularity, e.g. minute buckets from one day -> one daily min/avg/max point. const getResponseTimeSeriesData = ( buckets: WebhookDeliveryStatsBucket[], @@ -347,6 +358,7 @@ export const WebhookOverviewContent = ({ : range === 'today' ? 'today' : 'daily' + const hasFailedDeliveries = buckets.some((bucket) => bucket.failed > 0) const deliverySeries = [ { name: 'Total deliveries', @@ -362,10 +374,17 @@ export const WebhookOverviewContent = ({ name: 'Failed deliveries', colorVar: '--accent-error-highlight', showSymbol: true, - z: 1, + z: hasFailedDeliveries ? 3 : 1, data: buckets.length > 0 - ? getDeliveryCountSeriesData(buckets, rangeBounds, grouping, 'failed') + ? hideInactiveZeroValuePoints( + getDeliveryCountSeriesData( + buckets, + rangeBounds, + grouping, + 'failed' + ) + ) : [], }, ] satisfies WebhookStatsChartSeries[] From 43da1be718dd173390b63a78f0395ebd31f3bf0c Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 20 May 2026 16:06:17 -0400 Subject: [PATCH 79/90] refactor(webhooks): improve logic for hiding inactive zero-value points in delivery chart - Updated the function to determine visibility of zero-value points by checking for nearby non-zero values, enhancing the accuracy of data representation. - This change aims to further reduce clutter in the delivery chart, improving clarity and user experience. --- .../settings/webhooks/detail/overview-content.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx index 079d9201b..4644cff9a 100644 --- a/src/features/dashboard/settings/webhooks/detail/overview-content.tsx +++ b/src/features/dashboard/settings/webhooks/detail/overview-content.tsx @@ -224,9 +224,10 @@ const hideInactiveZeroValuePoints = (points: WebhookStatsChartPoint[]) => points.map((point, index) => { if (point.value !== 0) return point - const hasAdjacentValue = - (points[index - 1]?.value ?? 0) > 0 || (points[index + 1]?.value ?? 0) > 0 - if (hasAdjacentValue) return point + const hasNearbyValue = [-2, -1, 1, 2].some( + (offset) => (points[index + offset]?.value ?? 0) > 0 + ) + if (hasNearbyValue) return point return { ...point, synthetic: true, value: null } }) From 51cf737e4edc6cf764da651715bcffe53ec12fab Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 20 May 2026 16:16:45 -0400 Subject: [PATCH 80/90] refactor(webhooks): remove webhook delivery retrieval functionality - Eliminated the `getWebhookDelivery` function and its associated input schema from the webhooks repository and API router. - Updated the OpenAPI specification to remove the endpoint for retrieving webhook delivery attempts, streamlining the API. - Adjusted TypeScript types and schemas to reflect the removal of delivery ID parameters, enhancing clarity and maintainability. These changes aim to simplify the webhook delivery interface and improve overall code organization. --- spec/openapi.argus.yaml | 33 -------------- .../modules/webhooks/repository.server.ts | 36 --------------- src/core/server/api/routers/webhooks.ts | 31 ------------- src/core/server/functions/webhooks/schema.ts | 8 ---- src/core/shared/contracts/argus-api.types.ts | 44 ------------------- 5 files changed, 152 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index bbfbb7055..2d260714c 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -46,13 +46,6 @@ components: schema: type: string format: uuid - deliveryID: - name: deliveryID - in: path - required: true - schema: - type: string - format: uuid responses: "400": @@ -761,32 +754,6 @@ paths: "500": $ref: "#/components/responses/500" - /events/webhooks/{webhookID}/deliveries/{deliveryID}: - get: - operationId: webhookDeliveryGet - description: Get a webhook delivery attempt. - tags: [webhooks] - security: - - ApiKeyAuth: [] - - Supabase1TokenAuth: [] - Supabase2TeamAuth: [] - parameters: - - $ref: "#/components/parameters/webhookID" - - $ref: "#/components/parameters/deliveryID" - responses: - "200": - description: Webhook delivery attempt. - content: - application/json: - schema: - $ref: "#/components/schemas/WebhookDelivery" - "404": - $ref: "#/components/responses/404" - "401": - $ref: "#/components/responses/401" - "500": - $ref: "#/components/responses/500" - /events/webhooks/{webhookID}/stats: get: operationId: webhookDeliveryStats diff --git a/src/core/modules/webhooks/repository.server.ts b/src/core/modules/webhooks/repository.server.ts index 30ec54074..c422d7f95 100644 --- a/src/core/modules/webhooks/repository.server.ts +++ b/src/core/modules/webhooks/repository.server.ts @@ -40,11 +40,6 @@ interface ListWebhookDeliveriesResult { nextCursor: string | null } -export interface GetWebhookDeliveryInput { - webhookId: string - deliveryId: string -} - export interface GetWebhookDeliveryStatsInput { webhookId: string start?: string @@ -61,9 +56,6 @@ export interface WebhooksRepository { listWebhookDeliveries( input: ListWebhookDeliveriesInput ): Promise> - getWebhookDelivery( - input: GetWebhookDeliveryInput - ): Promise> getWebhookDeliveryStats( input: GetWebhookDeliveryStatsInput ): Promise> @@ -171,34 +163,6 @@ export function createWebhooksRepository( nextCursor: response.response.headers.get('X-Next-Cursor'), }) }, - async getWebhookDelivery(input) { - const response = await deps.infraClient.GET( - '/events/webhooks/{webhookID}/deliveries/{deliveryID}', - { - headers: { - ...deps.authHeaders(scope.accessToken, scope.teamId), - }, - params: { - path: { - webhookID: input.webhookId, - deliveryID: input.deliveryId, - }, - }, - } - ) - - if (!response.response.ok || response.error) { - return err( - repoErrorFromHttp( - response.response.status, - response.error?.message ?? 'Failed to get webhook delivery', - response.error - ) - ) - } - - return ok(response.data) - }, async getWebhookDeliveryStats(input) { const response = await deps.infraClient.GET( '/events/webhooks/{webhookID}/stats', diff --git a/src/core/server/api/routers/webhooks.ts b/src/core/server/api/routers/webhooks.ts index 9e5862164..0d8c79ee6 100644 --- a/src/core/server/api/routers/webhooks.ts +++ b/src/core/server/api/routers/webhooks.ts @@ -3,7 +3,6 @@ import { throwTRPCErrorFromRepoError } from '@/core/server/adapters/errors' import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/repository' import { DeleteWebhookInputSchema, - GetWebhookDeliveryInputSchema, GetWebhookDeliveryStatsInputSchema, GetWebhookInputSchema, ListWebhookDeliveriesInputSchema, @@ -139,36 +138,6 @@ export const webhooksRouter = createTRPCRouter({ } }), - getDelivery: webhooksRepositoryProcedure - .input(GetWebhookDeliveryInputSchema) - .query(async ({ ctx, input }) => { - const result = await ctx.webhooksRepository.getWebhookDelivery({ - webhookId: input.webhookId, - deliveryId: input.deliveryId, - }) - - if (!result.ok) { - l.error( - { - key: 'get_webhook_delivery_trpc:error', - status: result.error.status, - error: result.error, - team_id: ctx.teamId, - user_id: ctx.session.user.id, - context: { - webhookId: input.webhookId, - deliveryId: input.deliveryId, - }, - }, - `Failed to get webhook delivery: ${result.error.status}: ${result.error.message}` - ) - - throwTRPCErrorFromRepoError(result.error) - } - - return { delivery: result.data } - }), - getDeliveryStats: webhooksRepositoryProcedure .input(GetWebhookDeliveryStatsInputSchema) .query(async ({ ctx, input }) => { diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index 84ef62fbd..ef4aeb257 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -62,11 +62,6 @@ export const ListWebhookDeliveriesInputSchema = z.object({ eventType: z.array(SandboxLifecycleEventTypeSchema).optional(), }) -export const GetWebhookDeliveryInputSchema = z.object({ - webhookId: z.uuid(), - deliveryId: z.uuid(), -}) - export const GetWebhookDeliveryStatsInputSchema = z .object({ webhookId: z.uuid(), @@ -96,9 +91,6 @@ export type GetWebhookInput = z.input export type ListWebhookDeliveriesInput = z.input< typeof ListWebhookDeliveriesInputSchema > -export type GetWebhookDeliveryInput = z.input< - typeof GetWebhookDeliveryInputSchema -> export type GetWebhookDeliveryStatsInput = z.input< typeof GetWebhookDeliveryStatsInputSchema > diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index 7beba8bce..ef0591434 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -181,23 +181,6 @@ export interface paths { patch?: never trace?: never } - '/events/webhooks/{webhookID}/deliveries/{deliveryID}': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** @description Get a webhook delivery attempt. */ - get: operations['webhookDeliveryGet'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/events/webhooks/{webhookID}/stats': { parameters: { query?: never @@ -491,7 +474,6 @@ export interface components { parameters: { sandboxID: string webhookID: string - deliveryID: string } requestBodies: never headers: never @@ -669,32 +651,6 @@ export interface operations { 500: components['responses']['500'] } } - webhookDeliveryGet: { - parameters: { - query?: never - header?: never - path: { - webhookID: components['parameters']['webhookID'] - deliveryID: components['parameters']['deliveryID'] - } - cookie?: never - } - requestBody?: never - responses: { - /** @description Webhook delivery attempt. */ - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['WebhookDelivery'] - } - } - 401: components['responses']['401'] - 404: components['responses']['404'] - 500: components['responses']['500'] - } - } webhookDeliveryStats: { parameters: { query?: { From aa8b50ab364e50ded6fb57df7cc26c4a76a935c2 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 21 May 2026 11:05:43 -0400 Subject: [PATCH 81/90] refactor(webhooks): update data types for webhook delivery statistics - Changed the data types for minimum and maximum properties in the OpenAPI specification and TypeScript types from integer to number with double format for improved precision. - This update enhances the accuracy of webhook delivery statistics representation, aligning with best practices for numerical data handling. --- spec/openapi.argus.yaml | 8 ++++---- src/core/shared/contracts/argus-api.types.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index 2d260714c..e14e978bc 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -377,14 +377,14 @@ components: - maximum properties: minimum: - type: integer - format: int32 + type: number + format: double average: type: number format: double maximum: - type: integer - format: int32 + type: number + format: double WebhookDeliveryStatsBucket: description: Webhook delivery stats for a time bucket diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index ef0591434..c785ca3b6 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -398,11 +398,11 @@ export interface components { } /** @description Webhook delivery duration statistics in milliseconds */ WebhookDeliveryDurationStats: { - /** Format: int32 */ + /** Format: double */ minimum: number /** Format: double */ average: number - /** Format: int32 */ + /** Format: double */ maximum: number } /** @description Webhook delivery stats for a time bucket */ From c9d4c857596f60dc7f73eaee7c46bdb55076e07f Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 21 May 2026 13:58:08 -0400 Subject: [PATCH 82/90] Comply with nextCursor in payload --- spec/openapi.argus.yaml | 26 ++++++++++++------- .../modules/webhooks/repository.server.ts | 4 +-- src/core/shared/contracts/argus-api.types.ts | 12 ++++++--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index e14e978bc..4cc96c84a 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -426,6 +426,21 @@ components: items: $ref: "#/components/schemas/WebhookDelivery" + WebhookDeliveriesListPayload: + description: Paginated webhook delivery attempts grouped by event + required: + - data + - nextCursor + properties: + data: + type: array + items: + $ref: "#/components/schemas/WebhookDeliveryEvent" + nextCursor: + type: string + nullable: true + description: Cursor to pass to the next list request, or null when there is no next page. + paths: /health: get: @@ -680,7 +695,7 @@ paths: required: false schema: type: string - description: Opaque cursor from the X-Next-Cursor response header. + description: Opaque cursor from the previous response's nextCursor field. - name: limit in: query required: false @@ -734,17 +749,10 @@ paths: responses: "200": description: List of webhook delivery attempts grouped by event. - headers: - X-Next-Cursor: - description: Cursor to pass to the next list request, omitted when there is no next page. - schema: - type: string content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/WebhookDeliveryEvent" + $ref: "#/components/schemas/WebhookDeliveriesListPayload" "400": $ref: "#/components/responses/400" "404": diff --git a/src/core/modules/webhooks/repository.server.ts b/src/core/modules/webhooks/repository.server.ts index c422d7f95..7c9863290 100644 --- a/src/core/modules/webhooks/repository.server.ts +++ b/src/core/modules/webhooks/repository.server.ts @@ -159,8 +159,8 @@ export function createWebhooksRepository( } return ok({ - data: response.data ?? [], - nextCursor: response.response.headers.get('X-Next-Cursor'), + data: response.data?.data ?? [], + nextCursor: response.data?.nextCursor ?? null, }) }, async getWebhookDeliveryStats(input) { diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index c785ca3b6..f2b3b8b53 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -423,6 +423,12 @@ export interface components { sandboxId: string attempts: components['schemas']['WebhookDelivery'][] } + /** @description Paginated webhook delivery attempts grouped by event */ + WebhookDeliveriesListPayload: { + data: components['schemas']['WebhookDeliveryEvent'][] + /** @description Cursor to pass to the next list request, or null when there is no next page. */ + nextCursor: string | null + } } responses: { /** @description Bad request */ @@ -613,7 +619,7 @@ export interface operations { webhookDeliveriesList: { parameters: { query?: { - /** @description Opaque cursor from the X-Next-Cursor response header. */ + /** @description Opaque cursor from the previous response's nextCursor field. */ cursor?: string limit?: number orderAsc?: boolean @@ -637,12 +643,10 @@ export interface operations { /** @description List of webhook delivery attempts grouped by event. */ 200: { headers: { - /** @description Cursor to pass to the next list request, omitted when there is no next page. */ - 'X-Next-Cursor'?: string [name: string]: unknown } content: { - 'application/json': components['schemas']['WebhookDeliveryEvent'][] + 'application/json': components['schemas']['WebhookDeliveriesListPayload'] } } 400: components['responses']['400'] From c0cb9e0d9e30b7dc3ed616ce7c65d798e638d86e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 21 May 2026 15:12:24 -0400 Subject: [PATCH 83/90] fix(webhooks): organize imports after main merge --- .../dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx index c4d7620ae..e49eaebeb 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx @@ -33,8 +33,8 @@ import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' import { Separator } from '@/ui/primitives/separator' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' import { - WEBHOOK_EXAMPLE_PAYLOAD, WEBHOOK_EVENT_LABELS, + WEBHOOK_EXAMPLE_PAYLOAD, WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL, } from './constants' From a00d82b8a9b7719dfbf68841ebe1da6f884df04f Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 21 May 2026 15:22:24 -0400 Subject: [PATCH 84/90] refactor(webhooks): reuse upsert schema types --- .../modules/webhooks/repository.server.ts | 11 +-------- src/core/server/api/routers/webhooks.ts | 23 +++++++------------ src/core/server/functions/webhooks/schema.ts | 3 ++- .../webhooks/upsert-webhook-dialog-steps.tsx | 4 ++-- .../webhooks/upsert-webhook-dialog.tsx | 6 ++--- 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/core/modules/webhooks/repository.server.ts b/src/core/modules/webhooks/repository.server.ts index 4102832d1..66c6c25d5 100644 --- a/src/core/modules/webhooks/repository.server.ts +++ b/src/core/modules/webhooks/repository.server.ts @@ -1,6 +1,7 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import type { UpsertWebhookInput } from '@/core/server/functions/webhooks/schema' import { infra } from '@/core/shared/clients/api' import type { components as ArgusComponents } from '@/core/shared/contracts/argus-api.types' import { repoErrorFromHttp } from '@/core/shared/errors' @@ -14,16 +15,6 @@ type WebhooksRepositoryDeps = { export type WebhooksScope = TeamRequestScope -export interface UpsertWebhookInput { - mode: 'create' | 'update' - webhookId?: string - name: string - url: string - events: string[] - signatureSecret?: string - enabled: boolean -} - export interface WebhooksRepository { listWebhooks(): Promise< RepoResult diff --git a/src/core/server/api/routers/webhooks.ts b/src/core/server/api/routers/webhooks.ts index 096e95101..7adfbc1ff 100644 --- a/src/core/server/api/routers/webhooks.ts +++ b/src/core/server/api/routers/webhooks.ts @@ -42,33 +42,26 @@ export const webhooksRouter = createTRPCRouter({ upsert: webhooksRepositoryProcedure .input(UpsertWebhookInputSchema) .mutation(async ({ ctx, input }) => { - const { mode, webhookId, name, url, events, signatureSecret, enabled } = - input - - const result = await ctx.webhooksRepository.upsertWebhook({ - mode, - webhookId: webhookId ?? undefined, - name, - url, - events, - signatureSecret: signatureSecret ?? undefined, - enabled: enabled ?? true, - }) + const result = await ctx.webhooksRepository.upsertWebhook(input) if (!result.ok) { l.error( { key: - mode === 'update' + input.mode === 'update' ? 'update_webhook_trpc:error' : 'create_webhook_trpc:error', status: result.error.status, error: result.error, team_id: ctx.teamId, user_id: ctx.session.user.id, - context: { mode, name, events }, + context: { + mode: input.mode, + name: input.name, + events: input.events, + }, }, - `Failed to ${mode} webhook: ${result.error.status}: ${result.error.message}` + `Failed to ${input.mode} webhook: ${result.error.status}: ${result.error.message}` ) throwTRPCErrorFromRepoError(result.error) diff --git a/src/core/server/functions/webhooks/schema.ts b/src/core/server/functions/webhooks/schema.ts index 6e19e54ca..e31e35fab 100644 --- a/src/core/server/functions/webhooks/schema.ts +++ b/src/core/server/functions/webhooks/schema.ts @@ -45,7 +45,8 @@ export const UpdateWebhookSecretInputSchema = z.object({ signatureSecret: WebhookSecretSchema, }) -export type UpsertWebhookInput = z.input +export type UpsertWebhookFormInput = z.input +export type UpsertWebhookInput = z.output export type DeleteWebhookInput = z.input export type UpdateWebhookSecretInput = z.input< typeof UpdateWebhookSecretInputSchema diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx index e49eaebeb..0b9619bb5 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx @@ -10,7 +10,7 @@ import { type SandboxLifecycleEventType, SandboxLifecycleEventTypeSchema, } from '@/core/modules/sandboxes/lifecycle-event-types' -import type { UpsertWebhookInput } from '@/core/server/functions/webhooks/schema' +import type { UpsertWebhookFormInput } from '@/core/server/functions/webhooks/schema' import { useClipboard } from '@/lib/hooks/use-clipboard' import { Button } from '@/ui/primitives/button' import { Checkbox } from '@/ui/primitives/checkbox' @@ -44,7 +44,7 @@ export type SecretType = z.infer type UpsertWebhookDialogStepsProps = { currentStep: number - form: UseFormReturn + form: UseFormReturn isLoading: boolean selectedEvents: string[] exampleEventType: SandboxLifecycleEventType diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx index a4c43fc74..81645bd12 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx @@ -9,7 +9,7 @@ import { SandboxLifecycleEventTypeSchema, } from '@/core/modules/sandboxes/lifecycle-event-types' import { - type UpsertWebhookInput, + type UpsertWebhookFormInput, UpsertWebhookInputSchema, } from '@/core/server/functions/webhooks/schema' import { @@ -77,7 +77,7 @@ export function UpsertWebhookDialog({ teamSlug: team.slug, }).queryKey - const defaultValues: UpsertWebhookInput = { + const defaultValues: UpsertWebhookFormInput = { webhookId: isUpdateMode ? webhook?.id : undefined, mode, name: webhook?.name || '', @@ -91,7 +91,7 @@ export function UpsertWebhookDialog({ ...(isUpdateMode ? {} : { signatureSecret: '' }), } - const form = useForm({ + const form = useForm({ resolver: zodResolver(UpsertWebhookInputSchema), mode: 'onChange', disabled: !team.slug, From 7c0b26b22df54cf3ac6546815763a2d91e62002a Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 21 May 2026 15:38:35 -0400 Subject: [PATCH 85/90] refactor(webhooks): handle secret tab changes directly --- .../webhooks/upsert-webhook-dialog-steps.tsx | 32 +++++++------------ .../webhooks/upsert-webhook-dialog.tsx | 13 ++++++-- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx index 0b9619bb5..5a0d4f473 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx @@ -1,7 +1,7 @@ 'use client' import { AnimatePresence, motion } from 'motion/react' -import { useEffect, useMemo, useRef } from 'react' +import { useEffect, useRef } from 'react' import type { UseFormReturn } from 'react-hook-form' import ShikiHighlighter from 'react-shiki' import { z } from 'zod' @@ -52,6 +52,7 @@ type UpsertWebhookDialogStepsProps = { handleAllToggle: () => void handleEventToggle: (event: SandboxLifecycleEventType) => void mode: 'create' | 'update' + preGeneratedSecret: string secretType: SecretType onSecretTypeChange: (value: SecretType) => void hasCopied: boolean @@ -68,6 +69,7 @@ export function UpsertWebhookDialogSteps({ handleAllToggle, handleEventToggle, mode, + preGeneratedSecret, secretType, onSecretTypeChange, hasCopied, @@ -75,14 +77,6 @@ export function UpsertWebhookDialogSteps({ }: UpsertWebhookDialogStepsProps) { const shikiTheme = useShikiTheme() - const preGeneratedSecret = useMemo(() => { - const chars = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - const array = new Uint8Array(32) - crypto.getRandomValues(array) - return Array.from(array, (byte) => chars[byte % chars.length]).join('') - }, []) - const [copied, copy] = useClipboard() const customSecretInputRef = useRef(null) @@ -94,18 +88,19 @@ export function UpsertWebhookDialogSteps({ return () => window.clearTimeout(id) }, [secretType]) - // sync secret with form state and validation - only in create mode - // in update mode, we should never touch the signature secret - useEffect(() => { + const handleSecretTypeChange = (value: string) => { + const parsed = SecretTypeSchema.safeParse(value) + if (!parsed.success) return + + onSecretTypeChange(parsed.data) + if (mode !== 'create') return - if (secretType === 'pre-generated') { - // set pre-generated secret and trigger validation to clear any errors + if (parsed.data === 'pre-generated') { form.setValue('signatureSecret', preGeneratedSecret, { shouldValidate: true, shouldDirty: true, }) - // explicitly clear any errors since pre-generated is always valid form.clearErrors('signatureSecret') } else { form.setValue('signatureSecret', '', { @@ -113,7 +108,7 @@ export function UpsertWebhookDialogSteps({ shouldDirty: false, }) } - }, [mode, secretType, preGeneratedSecret, form]) + } useEffect(() => { if (copied) onCopied() @@ -292,10 +287,7 @@ export function UpsertWebhookDialogSteps({ {/* Tabs */} { - const parsed = SecretTypeSchema.safeParse(v) - if (parsed.success) onSecretTypeChange(parsed.data) - }} + onValueChange={handleSecretTypeChange} className="min-h-0 w-full flex-1 h-full" > diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx index 81645bd12..c021917e8 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { type SandboxLifecycleEventType, @@ -77,6 +77,14 @@ export function UpsertWebhookDialog({ teamSlug: team.slug, }).queryKey + const preGeneratedSecret = useMemo(() => { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const array = new Uint8Array(32) + crypto.getRandomValues(array) + return Array.from(array, (byte) => chars[byte % chars.length]).join('') + }, []) + const defaultValues: UpsertWebhookFormInput = { webhookId: isUpdateMode ? webhook?.id : undefined, mode, @@ -88,7 +96,7 @@ export function UpsertWebhookDialog({ (event): event is SandboxLifecycleEventType => SandboxLifecycleEventTypeSchema.safeParse(event).success ) ?? [], - ...(isUpdateMode ? {} : { signatureSecret: '' }), + ...(isUpdateMode ? {} : { signatureSecret: preGeneratedSecret }), } const form = useForm({ @@ -276,6 +284,7 @@ export function UpsertWebhookDialog({ handleAllToggle={handleAllToggle} handleEventToggle={handleEventToggle} mode={mode} + preGeneratedSecret={preGeneratedSecret} secretType={secretType} onSecretTypeChange={setSecretType} hasCopied={hasCopied} From f326960768487358a72867f0fa7d5191817eaf25 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 21 May 2026 16:03:48 -0400 Subject: [PATCH 86/90] refactor(webhooks): simplify copied secret state --- .../webhooks/upsert-webhook-dialog-steps.tsx | 30 +++++++++++-------- .../webhooks/upsert-webhook-dialog.tsx | 14 +++++---- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx index 5a0d4f473..b925040b9 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog-steps.tsx @@ -52,11 +52,11 @@ type UpsertWebhookDialogStepsProps = { handleAllToggle: () => void handleEventToggle: (event: SandboxLifecycleEventType) => void mode: 'create' | 'update' + hasCopiedSecret: boolean + setHasCopiedSecret: (value: boolean) => void preGeneratedSecret: string secretType: SecretType onSecretTypeChange: (value: SecretType) => void - hasCopied: boolean - onCopied: () => void } export function UpsertWebhookDialogSteps({ @@ -69,15 +69,14 @@ export function UpsertWebhookDialogSteps({ handleAllToggle, handleEventToggle, mode, + hasCopiedSecret, + setHasCopiedSecret, preGeneratedSecret, secretType, onSecretTypeChange, - hasCopied, - onCopied, }: UpsertWebhookDialogStepsProps) { const shikiTheme = useShikiTheme() - - const [copied, copy] = useClipboard() + const [secretCopiedFeedback, copySecret] = useClipboard() const customSecretInputRef = useRef(null) useEffect(() => { @@ -110,9 +109,10 @@ export function UpsertWebhookDialogSteps({ } } - useEffect(() => { - if (copied) onCopied() - }, [copied, onCopied]) + const handleCopySecret = () => { + void copySecret(preGeneratedSecret) + setHasCopiedSecret(true) + } return ( @@ -323,18 +323,22 @@ export function UpsertWebhookDialogSteps({ diff --git a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx index c021917e8..1e8c0447c 100644 --- a/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/upsert-webhook-dialog.tsx @@ -65,7 +65,7 @@ export function UpsertWebhookDialog({ const [currentStep, setCurrentStep] = useState(1) const [lastSelectedEvent, setLastSelectedEvent] = useState(null) - const [hasCopied, setHasCopied] = useState(false) + const [hasCopiedSecret, setHasCopiedSecret] = useState(false) const [secretType, setSecretType] = useState('pre-generated') const [finishSetupDialogOpen, setFinishSetupDialogOpen] = useState(false) const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false) @@ -143,7 +143,7 @@ export function UpsertWebhookDialog({ const resetDialogState = () => { setCurrentStep(1) - setHasCopied(false) + setHasCopiedSecret(false) setSecretType('pre-generated') form.reset() upsertMutation.reset() @@ -284,11 +284,11 @@ export function UpsertWebhookDialog({ handleAllToggle={handleAllToggle} handleEventToggle={handleEventToggle} mode={mode} + hasCopiedSecret={hasCopiedSecret} + setHasCopiedSecret={setHasCopiedSecret} preGeneratedSecret={preGeneratedSecret} secretType={secretType} onSecretTypeChange={setSecretType} - hasCopied={hasCopied} - onCopied={() => setHasCopied(true)} />
@@ -332,7 +332,11 @@ export function UpsertWebhookDialog({