Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f8646d6
refactor: update webhooks page structure and remove unused components
sarimrmalik Apr 17, 2026
2e0e1da
refactor: simplify webhooks table and update styling
sarimrmalik Apr 17, 2026
9b80329
Merge remote-tracking branch 'origin/main' into refactor/webhooks-page
sarimrmalik Apr 23, 2026
99ebc97
Fix type errors
sarimrmalik Apr 23, 2026
d3cd38c
refactor: enhance layout and styling for dashboard components
sarimrmalik Apr 23, 2026
429f12e
refactor: enhance webhook add/edit dialog with example payload and ev…
sarimrmalik Apr 23, 2026
ab29a27
feat: add clearable input functionality to webhook dialog
sarimrmalik Apr 23, 2026
50b1a30
refactor: enhance webhook dialog with dynamic example payload
sarimrmalik Apr 24, 2026
ced46af
Merge remote-tracking branch 'origin/main' into refactor/webhooks-page
sarimrmalik Apr 24, 2026
333ad34
refactor: enhance webhook add/edit dialog with improved focus and sty…
sarimrmalik Apr 24, 2026
6c16231
feat: integrate clipboard functionality in webhook dialog
sarimrmalik Apr 24, 2026
63411f7
fix: ensure validation for webhook secret input
sarimrmalik Apr 27, 2026
0511804
Merge remote-tracking branch 'origin/main' into refactor/webhooks-page
sarimrmalik Apr 29, 2026
07d161e
refactor: overhaul webhooks management with TRPC integration
sarimrmalik Apr 29, 2026
ca97924
Merge remote-tracking branch 'origin/main' into refactor/webhooks-page
sarimrmalik May 4, 2026
59aa65d
feat: add maxLength to custom secret input in webhook dialog
sarimrmalik May 4, 2026
031df16
feat: integrate FinishWebhookSetupDialog for enhanced webhook setup e…
sarimrmalik May 4, 2026
a6eaccf
refactor: enhance WebhookTableRow component with improved structure a…
sarimrmalik May 4, 2026
8f32bf2
refactor: improve styling and structure of WebhooksTable and WebhookR…
sarimrmalik May 4, 2026
92c521a
refactor: update styling and structure of WebhookTableRow and Webhook…
sarimrmalik May 4, 2026
52bca58
Merge branch 'main' of https://github.com/e2b-dev/dashboard into refa…
sarimrmalik May 6, 2026
30a5425
refactor: update webhook management to use 'create' and 'update' modes
sarimrmalik May 6, 2026
2db48f4
refactor: update UserAvatar component to use 'label' prop instead of …
sarimrmalik May 7, 2026
ce17dd8
refactor: enhance WebhookTableRow with URL copy functionality
sarimrmalik May 7, 2026
1c45c47
refactor: enhance WebhookTableRow with event badges and tooltips
sarimrmalik May 7, 2026
8bf085d
feat: introduce SandboxLifecycleEventType for webhook management
sarimrmalik May 7, 2026
5e782c5
refactor: update WebhookTableRow and WebhooksTable for improved layou…
sarimrmalik May 7, 2026
ff7b46b
refactor: streamline WebhookTableRow actions and improve secret editi…
sarimrmalik May 7, 2026
42a4519
feat: add DiscardWebhookChangesDialog for unsaved changes confirmation
sarimrmalik May 7, 2026
3fdf280
refactor: replace WebhookDeleteDialog with DeleteWebhookDialog for im…
sarimrmalik May 7, 2026
9494a15
refactor: enhance webhook dialogs and table layout for improved usabi…
sarimrmalik May 7, 2026
d6af1ae
refactor: improve EditSecretDialog and UpsertWebhookDialog for better…
sarimrmalik May 7, 2026
07c1717
refactor: replace EditSecretDialog with UpdateWebhookSecretDialog for…
sarimrmalik May 7, 2026
43c62a3
refactor: enhance UpsertWebhookDialog and related components for impr…
sarimrmalik May 7, 2026
a29e59c
refactor: simplify button variant logic in UpsertWebhookDialog
sarimrmalik May 7, 2026
78094c5
refactor: enhance webhook upsert logic and validation
sarimrmalik May 7, 2026
7cc4045
refactor: update event handling and validation in UpsertWebhookDialog
sarimrmalik May 7, 2026
ec0a71c
feat: add clearable functionality to webhook input field in UpsertWeb…
sarimrmalik May 7, 2026
1edb57d
refactor: simplify WebhooksPageContent structure and enhance loading …
sarimrmalik May 7, 2026
29ec381
refactor: remove disabled prop from WebhooksSearchFieldShell for impr…
sarimrmalik May 7, 2026
a0b0b79
refactor: update UpsertWebhookDialog to improve event handling and de…
sarimrmalik May 7, 2026
14e3832
Remove unused component
sarimrmalik May 8, 2026
f072a70
refactor: update webhook context structure in webhooksRouter
sarimrmalik May 8, 2026
e4722c1
Merge remote-tracking branch 'origin/main' into refactor/webhooks-page
sarimrmalik May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/__test__/unit/sandbox-monitoring-chart-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 13 additions & 40 deletions src/app/dashboard/[teamSlug]/webhooks/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
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 { AddIcon } from '@/ui/primitives/icons'
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<{
Expand All @@ -25,36 +17,17 @@ export default async function WebhooksPage({
return notFound()
}

return (
<Frame
classNames={{
wrapper: 'w-full max-md:p-0',
frame: 'max-md:border-none',
}}
>
<Card className="w-full">
<CardHeader>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
<CardDescription className="max-w-[600px] text-fg">
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.
</CardDescription>
const { teamSlug } = await params

const webhooksResult = await getWebhooks({ teamSlug })

<WebhookAddEditDialog mode="add">
<Button className="w-full sm:w-auto sm:self-start">
<AddIcon className="size-4" /> Add Webhook
</Button>
</WebhookAddEditDialog>
</div>
</CardHeader>
const hasError = webhooksResult?.data === undefined

<CardContent>
<div className="w-full overflow-x-auto">
<WebhooksTable params={params} className="min-w-[900px]" />
</div>
</CardContent>
</Card>
</Frame>
const webhooks = webhooksResult?.data?.webhooks ?? []

return (
<Page>
<WebhooksPageContent hasError={hasError} webhooks={webhooks} />
</Page>
)
}
2 changes: 1 addition & 1 deletion src/core/server/functions/webhooks/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { TeamSlugSchema } from '@/core/shared/schemas/team'
const WebhookUrlSchema = z.httpUrl('Must be a valid URL').trim()
const WebhookSecretSchema = z
.string()
.min(32, 'Secret must be at least 32 characters')
.trim()
.min(32, 'Secret must be at least 32 characters')

export const UpsertWebhookSchema = z
.object({
Expand Down
159 changes: 97 additions & 62 deletions src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'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 ShikiHighlighter from 'react-shiki'
import { useShikiTheme } from '@/configs/shiki'
import type { UpsertWebhookSchemaType } 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'
import {
Expand All @@ -15,40 +14,80 @@
FormLabel,
FormMessage,
} from '@/ui/primitives/form'
import { CopyIcon, ExternalLinkIcon, 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 { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area'
import { Separator } from '@/ui/primitives/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs'
import {
WEBHOOK_DOCS_URL,
WEBHOOK_EVENT_LABELS,
WEBHOOK_EVENTS,
WEBHOOK_EXAMPLE_PAYLOAD,
WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL,
type WebhookEvent,
} from './constants'

type WebhookAddEditDialogStepsProps = {
currentStep: number
form: UseFormReturn<UpsertWebhookSchemaType>
isLoading: boolean
selectedEvents: string[]
exampleEventType: WebhookEvent
allEventsSelected: boolean
handleAllToggle: () => void
handleEventToggle: (event: string) => void
mode: 'add' | 'edit'
hasCopied: boolean
onCopied: () => void
}

const WebhookExamplePayload = ({ eventType }: { eventType: WebhookEvent }) => (
<div className="bg-bg border border-stroke flex w-full items-center px-4 py-2.5 font-mono text-[13px] leading-5 text-fg-secondary whitespace-pre-wrap">
<div className="flex-1 min-w-px">
<div>{'{'}</div>
<div>
{' '}
<span className="text-accent-main-highlight">{'"type"'}</span>
{`: "${eventType}",`}
</div>
<div>
{' '}
<span className="text-accent-main-highlight">{'"sandboxId"'}</span>
{': "<UUID>",'}
</div>
<div>
{' '}
<span className="text-accent-main-highlight">{'"timestamp"'}</span>
{': "<TIMESTAMP>",'}
</div>
<div className="text-fg-tertiary">
{' // ... more fields, '}
<a
href={WEBHOOK_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-fg"
>
see docs
</a>
</div>
<div>{'}'}</div>
</div>
</div>
)

export function WebhookAddEditDialogSteps({
currentStep,
form,
isLoading,
selectedEvents,
exampleEventType,
allEventsSelected,
handleAllToggle,
handleEventToggle,
mode,
hasCopied,
onCopied,
}: WebhookAddEditDialogStepsProps) {
const shikiTheme = useShikiTheme()
const [secretType, setSecretType] = useState<'pre-generated' | 'custom'>(
'pre-generated'
)
Expand All @@ -61,7 +100,16 @@
return Array.from(array, (byte) => chars[byte % chars.length]).join('')
}, [])

const [copied, setCopied] = useState(false)
const [copied, copy] = useClipboard(150)

const customSecretInputRef = useRef<HTMLInputElement>(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
Expand All @@ -77,23 +125,17 @@
// explicitly clear any errors since pre-generated is always valid
form.clearErrors('signatureSecret')
} else {
// clear for custom input
form.setValue('signatureSecret', '', {
shouldValidate: false,
shouldValidate: true,
shouldDirty: false,
})
}
}, [mode, secretType, preGeneratedSecret, form])

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(preGeneratedSecret)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
await copy(preGeneratedSecret)
setTimeout(onCopied, 150)
}

Check failure on line 138 in src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx

View check run for this annotation

Claude / Claude Code Review

hasCopied state can be set/unset incorrectly across the dialog flow

The new `hasCopied` parent state introduced in this refactor has three bugs that combine to make the Copy / Add button variants misleading. **(1)** `handleCopy` in `add-edit-dialog-steps.tsx:135-138` does `setTimeout(onCopied, 150)` without storing/clearing the id. The parent `WebhookAddEditDialog` stays mounted across opens, so if the user clicks Copy and closes the dialog within 150ms, `handleDialogChange(false)` resets `hasCopied` to false but the late timeout fires afterward and flips it ba

return (
<AnimatePresence mode="wait" initial={false}>
Expand All @@ -118,6 +160,8 @@
placeholder="Example webhook"
disabled={isLoading}
className="min-w-0"
clearable
onClear={() => field.onChange('')}
{...field}
/>
</FormControl>
Expand All @@ -138,6 +182,8 @@
placeholder="https://example.com/postreceive"
disabled={isLoading}
className="min-w-0"
clearable
onClear={() => field.onChange('')}
{...field}
/>
</FormControl>
Expand Down Expand Up @@ -188,7 +234,7 @@
htmlFor={`event-${event}`}
className="cursor-pointer select-none"
>
{event}
{WEBHOOK_EVENT_LABELS[event]}
</Label>
</div>
))}
Expand All @@ -201,27 +247,14 @@

{/* Description */}
<div className="flex flex-col gap-2 min-w-0">
<p className="text-fg-tertiary prose-body break-words">
<p className="text-fg-tertiary prose-body wrap-break-word">
We'll send a POST request with a JSON payload to{' '}
<span className="break-all text-fg">
<span className="break-all">
{form.watch('url') || 'https://example.com/postreceive'}
</span>{' '}
for each event. Example:
</p>
<div className="border overflow-hidden w-full">
<ScrollArea>
<ShikiHighlighter
language="json"
theme={shikiTheme}
className="px-3 py-2 text-xs"
addDefaultStyles={false}
showLanguage={false}
>
{WEBHOOK_EXAMPLE_PAYLOAD}
</ShikiHighlighter>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
<WebhookExamplePayload eventType={exampleEventType} />
</div>
</motion.div>
)}
Expand All @@ -237,24 +270,11 @@
>
{/* Section Title and Description */}
<div className="flex flex-col gap-2">
<p className="text-fg-secondary prose-label uppercase">
Signature Secret
</p>
<p className="text-fg-secondary prose-label uppercase">Secret</p>
<p className="text-fg-tertiary prose-body">
This secret is used to verify webhook authenticity. Each request
includes an <code className="text-fg">e2b-signature</code> 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.
</p>
<a
href={WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="text-fg-link hover:text-fg-link-hover prose-body inline-flex items-center gap-1 w-fit"
>
View validation examples
<ExternalLinkIcon className="size-3" />
</a>
</div>

{/* Tabs */}
Expand Down Expand Up @@ -298,19 +318,27 @@
</FormControl>
<Button
type="button"
variant={hasCopied ? 'secondary' : 'primary'}
onClick={handleCopy}
disabled={isLoading}
className="shrink-0"
className="shrink-0 relative"
>
<CopyIcon className="size-4" />
{copied ? 'Copied' : 'Copy'}
<span
className={copied ? 'invisible contents' : 'contents'}
>
<CopyIcon className="size-4" />
Copy
</span>
{copied && (
<CheckIcon className="absolute size-4 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
)}
</Button>
</div>
<div className="flex gap-2 items-start">
<WarningIcon className="size-4 text-accent-warning-highlight shrink-0 mt-0.5" />
<div className="flex gap-1.5 items-center">
<WarningIcon className="size-4 text-accent-warning-highlight shrink-0" />
<p className="text-fg-secondary prose-body">
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.
</p>
</div>
</div>
Expand All @@ -331,12 +359,19 @@
disabled={isLoading}
className="min-w-0"
{...field}
ref={(el) => {
field.ref(el)
customSecretInputRef.current = el
}}
/>
</FormControl>
<p className="text-fg-tertiary prose-body">
{'> 32 characters'}
</p>
<FormMessage />
{form.formState.errors.signatureSecret ? (
<FormMessage />
) : (
<p className="text-fg-tertiary prose-body">
{'> 32 characters'}
</p>
)}
</FormItem>
)}
/>
Expand Down
Loading
Loading