Files
supabase/apps/studio/components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointDetails.tsx
Danny White 611ad867be chore(studio): webhook name (#43796)
## What kind of change does this PR introduce?

Chore

## What is the current behavior?

Endpoints have no (public) unique identifier, making it hard to
distinguish in UI.

## What is the new behavior?

Endpoints now have a name field. This is optional but strongly
encouraged:
- No _(Optional)_ on label
- Autogenerated name

Removing an endpoint name makes corresponding UI fall back the URL.

| Before | After |
| --- | --- |
| <img width="1234" height="629" alt="Settings Oldie 2 Toolshed
Supabase-CF557DE5-FA03-40E7-938E-2CC87F27C9A5"
src="https://github.com/user-attachments/assets/4b55fcb7-f8bc-4b85-a374-9ee62e452cb0"
/> | <img width="1234" height="629" alt="Settings Oldie 2 Toolshed
Supabase-74BBF142-DEC0-4626-B614-B05EBB2AC0EF"
src="https://github.com/user-attachments/assets/7ff40a91-c71a-4e20-85f8-0837d62202aa"
/> |
| <img width="1234" height="629" alt="Settings Oldie 2 Toolshed
Supabase-32A4B56C-61F5-4C1C-81DD-C455835E075F"
src="https://github.com/user-attachments/assets/f732ca50-2c60-4fbc-99c7-53afb5c34682"
/> | <img width="1234" height="629" alt="Settings Oldie 2 Toolshed
Supabase-29987973-9616-4409-9DF1-B9581112D4C3"
src="https://github.com/user-attachments/assets/0cf1db9a-fe84-4140-a2fc-bf10cf295c3b"
/> |
2026-03-18 03:08:56 +00:00

209 lines
7.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getStatusLevel } from 'components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DataTableColumnStatusCode } from 'components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { RotateCcw, Search } from 'lucide-react'
import {
Badge,
Card,
CardContent,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import type { WebhookDelivery, WebhookEndpoint } from './PlatformWebhooks.types'
import { statusBadgeVariant } from './PlatformWebhooksView.utils'
interface DetailItemProps {
label: string
children: React.ReactNode
ddClassName?: string
}
const DetailItem = ({ label, children, ddClassName = 'text-sm' }: DetailItemProps) => (
<div className="space-y-1">
<dt className="text-sm text-foreground-lighter">{label}</dt>
<dd className={ddClassName}>{children}</dd>
</div>
)
interface PlatformWebhooksEndpointDetailsProps {
deliverySearch: string
filteredDeliveries: WebhookDelivery[]
selectedEndpoint: WebhookEndpoint
onDeliverySearchChange: (value: string) => void
onOpenDelivery: (deliveryId: string) => void
onRetryDelivery: (deliveryId: string) => void
}
export const PlatformWebhooksEndpointDetails = ({
deliverySearch,
filteredDeliveries,
selectedEndpoint,
onDeliverySearchChange,
onOpenDelivery,
onRetryDelivery,
}: PlatformWebhooksEndpointDetailsProps) => {
const hasCustomHeaders = selectedEndpoint.customHeaders.length > 0
const hasName = selectedEndpoint.name.trim().length > 0
const hasDescription = selectedEndpoint.description.trim().length > 0
return (
<div className="space-y-16">
<div className="space-y-4">
<h2 className="text-foreground text-xl">Overview</h2>
<Card className="overflow-hidden">
<CardContent className="pb-5">
<dl className="grid grid-cols-1 gap-x-10 gap-y-6 md:grid-cols-2">
{hasName && <DetailItem label="Name">{selectedEndpoint.name}</DetailItem>}
<DetailItem label="URL" ddClassName="text-sm break-all">
{selectedEndpoint.url}
</DetailItem>
{hasDescription && (
<DetailItem label="Description">{selectedEndpoint.description}</DetailItem>
)}
<DetailItem label="Event types" ddClassName="flex flex-wrap gap-2">
{(selectedEndpoint.eventTypes.includes('*')
? ['All events (*)']
: selectedEndpoint.eventTypes
).map((eventType) => (
<code
key={eventType}
className="text-code-inline rounded-md border px-3 py-1.5 text-2xs"
>
{eventType}
</code>
))}
</DetailItem>
{hasCustomHeaders && (
<DetailItem label="Custom headers">
<div className="rounded-md border divide-y divide-border">
{selectedEndpoint.customHeaders.map((header) => (
<div
key={header.id}
className="px-2 py-2 font-mono font-medium text-xs flex items-center gap-2 flex-wrap"
>
<code className="text-code_block-4">{header.key}:</code>
<code className="">{header.value}</code>
</div>
))}
</div>
</DetailItem>
)}
<DetailItem label="Created by">{selectedEndpoint.createdBy}</DetailItem>
<DetailItem label="Created at">
<TimestampInfo className="text-sm" utcTimestamp={selectedEndpoint.createdAt} />
</DetailItem>
</dl>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<h2 className="text-foreground text-xl">Deliveries</h2>
<div className="flex items-center justify-between gap-2">
<Input
placeholder="Search deliveries"
size="tiny"
icon={<Search />}
value={deliverySearch}
className="w-full lg:w-52"
onChange={(event) => onDeliverySearchChange(event.target.value)}
/>
<p className="text-sm text-foreground-muted">{filteredDeliveries.length} total</p>
</div>
<Card className="overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Status</TableHead>
<TableHead>Event type</TableHead>
<TableHead>Response</TableHead>
<TableHead>Attempted</TableHead>
<TableHead className="w-1">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDeliveries.length > 0 ? (
filteredDeliveries.map((delivery) => (
<TableRow
key={delivery.id}
className="cursor-pointer inset-focus"
onClick={() => onOpenDelivery(delivery.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onOpenDelivery(delivery.id)
}
}}
tabIndex={0}
>
<TableCell>
<Badge variant={statusBadgeVariant[delivery.status]}>{delivery.status}</Badge>
</TableCell>
<TableCell>
<code className="text-code-inline">{delivery.eventType}</code>
</TableCell>
<TableCell>
{delivery.responseCode ? (
<DataTableColumnStatusCode
value={delivery.responseCode}
level={getStatusLevel(delivery.responseCode)}
className="text-xs"
/>
) : (
<span className="text-xs text-foreground-muted">–</span>
)}
</TableCell>
<TableCell>
<TimestampInfo className="text-sm" utcTimestamp={delivery.attemptAt} />
</TableCell>
<TableCell className="w-1 text-right">
<div className="flex h-full items-center justify-end">
{delivery.status !== 'success' && (
<ButtonTooltip
type="default"
className="w-7 shrink-0 hit-area-2"
icon={<RotateCcw size={14} />}
aria-label={`Retry ${delivery.id}`}
tooltip={{ content: { side: 'top', text: 'Retry' } }}
onClick={(event) => {
event.stopPropagation()
onRetryDelivery(delivery.id)
}}
onKeyDown={(event) => event.stopPropagation()}
/>
)}
{delivery.status === 'success' && (
<span aria-hidden className="size-7 shrink-0" />
)}
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5}>No deliveries found</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</div>
</div>
)
}