chore(studio): webhooks UI cleanup (#43797)

## What kind of change does this PR introduce?

UI updates to platform webhooks.

## What is the new behavior?

- `Webhooks` icon on empty state
- Better text wrapping/overflow handling on table
- Clearer endpoint deletion dialog

| After |
| --- |
| <img width="1024" height="563" alt="Webhooks Toolshed
Supabase-A3AF1CE1-B489-450F-9FD7-AE1B1BC0C999"
src="https://github.com/user-attachments/assets/6606922d-fd39-400e-bc2d-99806755fc09"
/> |
| <img width="1024" height="563" alt="Webhooks Toolshed
Supabase-00EF8F02-A3F2-477C-ADDB-68996D6C16E3"
src="https://github.com/user-attachments/assets/705a1d78-4366-4ea4-b538-99eb368a3997"
/> |
| <img width="1024" height="563" alt="Webhooks Toolshed
Supabase-902AC0EC-D0D2-471A-B241-C90FA8F50883"
src="https://github.com/user-attachments/assets/97ff1267-b31b-442f-ad14-62b0bd24ac46"
/> |
This commit is contained in:
Danny White authored and GitHub committed 2026-03-26 10:29:23 +11:00
1 parent 9da28685ec
commit 10e9cdd1f4
4 files changed
+29 -27

No files matched your search

@@ -30,7 +30,7 @@ export const PLATFORM_WEBHOOKS_MOCK_DATA: Record<WebhookScope, PlatformWebhooksM
{
id: '1a4e8c73-5b29-44af-8c62-9f1d2b3c4d5e',
name: 'Slack notifications',
url: 'https://hooks.slack.com/services/T000/B000/ORG',
url: 'https://hooks.slack.com/services/abc1234/def5678/ghi9012',
description: 'Operational alerts for organization-level events',
enabled: false,
eventTypes: ['organization.updated'],
@@ -1,5 +1,5 @@
import { createNavigationHandler } from 'lib/navigation'
import { ChevronRight, Eye, MoreVertical, Plus, Search, Trash2 } from 'lucide-react'
import { ChevronRight, Eye, MoreVertical, Plus, Search, Trash2, Webhook } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import {
@@ -93,6 +93,7 @@ export const PlatformWebhooksEndpointList = ({
<div className="flex items-center justify-between gap-2">
<h2 className="text-foreground text-xl">Endpoints</h2>
</div>
<div className="flex items-center justify-between gap-x-2">
<Input
placeholder="Search endpoints"
@@ -102,6 +103,7 @@ export const PlatformWebhooksEndpointList = ({
className="w-full lg:w-52"
onChange={(event) => onSearchChange(event.target.value)}
/>
<Button type="primary" icon={<Plus />} onClick={onCreateEndpoint}>
New endpoint
</Button>
@@ -109,6 +111,7 @@ export const PlatformWebhooksEndpointList = ({
{filteredEndpoints.length === 0 ? (
<EmptyStatePresentational
icon={Webhook}
title="No endpoints yet"
description="Create an endpoint to start receiving webhook deliveries."
>
@@ -171,15 +174,17 @@ export const PlatformWebhooksEndpointList = ({
<TableCell className="max-w-[420px]">
<p className="truncate">{displayName}</p>
{hasName && (
<p className="text-xs text-foreground-lighter truncate">{endpoint.url}</p>
<p className="text-xs text-foreground-lighter truncate mt-0.5">
{endpoint.url}
</p>
)}
</TableCell>
<TableCell className="max-w-[280px] truncate">
{formatEventCount(endpoint.eventTypes)}
</TableCell>
<TableCell>
<TableCell className="max-w-[220px]">
<TimestampInfo className="text-sm" utcTimestamp={endpoint.createdAt} />
<p className="text-xs text-foreground-lighter mt-0.5">
<p className="text-xs text-foreground-lighter mt-0.5 truncate">
by {endpoint.createdBy}
</p>
</TableCell>
@@ -74,18 +74,7 @@ export const PlatformWebhooksHeader = ({
<PageHeaderDescription>{headerDescription}</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
{hasSelectedEndpoint ? (
endpointActions
) : (
<>
<DocsButton href="https://supabase.com/docs" />
<Button asChild type="default">
<a target="_blank" rel="noopener noreferrer" href="https://supabase.com">
Leave feedback
</a>
</Button>
</>
)}
{hasSelectedEndpoint ? endpointActions : <DocsButton href="https://supabase.com/docs" />}
</PageHeaderAside>
</PageHeaderMeta>
</PageHeader>
@@ -128,9 +128,18 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
() => endpoints.find((endpoint) => endpoint.id === endpointIdPendingDelete) ?? null,
[endpoints, endpointIdPendingDelete]
)
const endpointPendingDeleteHasName = endpointPendingDelete
? endpointPendingDelete.name.trim().length > 0
: false
const endpointPendingDeleteDisplayName = endpointPendingDelete
? getWebhookEndpointDisplayName(endpointPendingDelete)
: ''
let deleteEndpointDescription = 'This action cannot be undone.'
if (endpointPendingDelete) {
deleteEndpointDescription = endpointPendingDeleteHasName
? `Deleting “${endpointPendingDeleteDisplayName}” stops all deliveries to the URL below. This can’t be undone.`
: 'Deleting this endpoint stops all deliveries to the URL below. This can’t be undone.'
}
useEffect(() => {
if (!platformWebhooksEnabled) {
@@ -233,7 +242,7 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
setDeliverySearch('')
}
setEndpointIdPendingDelete(null)
toast.success(`Deleted endpoint "${endpointPendingDeleteDisplayName}"`)
toast.success('Endpoint deleted')
}
const handleUpsertEndpoint = (values: EndpointFormValues) => {
@@ -333,14 +342,14 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
className="gap-x-2"
onClick={() => setShowRegenerateSecretConfirm(true)}
>
<RotateCw size={14} />
<RotateCw size={14} className="text-foreground-lighter" />
<span>Regenerate secret</span>
</DropdownMenuItem>
<DropdownMenuItem
className="gap-x-2"
onClick={() => setEndpointIdPendingDelete(selectedEndpoint.id)}
>
<Trash2 size={14} />
<Trash2 size={14} className="text-foreground-lighter" />
<span>Delete endpoint</span>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -420,14 +429,13 @@ export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPage
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete endpoint</AlertDialogTitle>
<AlertDialogDescription>
{endpointPendingDelete
? endpointPendingDelete.name.trim().length > 0
? `This will delete endpoint "${endpointPendingDeleteDisplayName}" with URL ${endpointPendingDelete.url}. This action cannot be undone.`
: `This will delete endpoint ${endpointPendingDelete.url}. This action cannot be undone.`
: 'This action cannot be undone.'}
</AlertDialogDescription>
<AlertDialogDescription>{deleteEndpointDescription}</AlertDialogDescription>
</AlertDialogHeader>
{endpointPendingDelete && (
<pre className="mx-5 -mt-1 mb-5 overflow-auto whitespace-nowrap rounded-md border border-muted bg-surface-200 px-4 py-3 font-mono text-xs tracking-tight text-foreground">
{endpointPendingDelete.url}
</pre>
)}
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" onClick={handleDeleteEndpoint}>