Files
supabase/apps/studio/components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointDetails.tsx
Danny White 476d4a5851 refactor(ui): drop redundant Button variant="default" props (#50161)
## What kind of change does this PR introduce?

Mechanical cleanup on top of the Button default-variant change (#50160).

## What is the current behavior?

Many callsites still pass `variant="default"` even though that is now
the component default.

## What is the new behavior?

Removes redundant static `variant="default"` from legacy `Button` and
`ButtonTooltip` callsites. Keeps explicit defaults where they document
the API:

- `button-default.tsx` and `button-sizes.tsx` demos
- `DocsButton`, which pins neutral styling at the wrapper boundary

## To test

Studio:

- [Auth → Rate
Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits):
dirty the form so Cancel appears; Cancel stays neutral, Save stays green
- [Project Settings → API
Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys):
`DocsButton` in the header actions stays neutral

Design system:

- [Design system →
Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button):
`button-default` / `button-sizes` still show explicit default styling;
Primary (green) is restricted to the Primary section (and `asChild`)

WWW:

- [www → Brand
assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets):
Download logo kit / Download button kit stay neutral
2026-09-11 17:05:26 +10:00

387 lines
14 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 {
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type PaginationState,
type SortingState,
} from '@tanstack/react-table'
import { ChevronLeft, ChevronRight, Copy, RotateCcw, Search } from 'lucide-react'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import {
Badge,
Button,
Card,
CardContent,
CardFooter,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { TanStackTableHeadSort } from 'ui-patterns/Table'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import type { WebhookDelivery, WebhookEndpoint } from './PlatformWebhooks.types'
import { statusBadgeVariant } from './PlatformWebhooksView.utils'
import { getStatusLevel } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DataTableColumnStatusCode } from '@/components/ui/DataTable/DataTableColumn/DataTableColumnStatusCode'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { onSearchInputEscape } from '@/lib/keyboard'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
interface DetailItemProps {
label: string
children: 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
onCopyUrl: () => void
onDeliverySearchChange: (value: string) => void
onOpenDelivery: (deliveryId: string) => void
onRetryDelivery: (deliveryId: string) => void
}
const DELIVERIES_PAGE_SIZE = 5
const DELIVERY_ACTIONS_COLUMN_ID = 'actions'
const DEFAULT_DELIVERY_SORTING: SortingState = [{ id: 'attemptAt', desc: true }]
const DELIVERY_COLUMNS: ColumnDef<WebhookDelivery>[] = [
{
accessorKey: 'status',
header: ({ column }) => <TanStackTableHeadSort column={column}>Status</TanStackTableHeadSort>,
cell: ({ row }) => (
<Badge variant={statusBadgeVariant[row.original.status]}>{row.original.status}</Badge>
),
},
{
accessorKey: 'eventType',
header: ({ column }) => (
<TanStackTableHeadSort column={column}>Event type</TanStackTableHeadSort>
),
cell: ({ row }) => <code className="text-code-inline">{row.original.eventType}</code>,
},
{
accessorKey: 'responseCode',
header: ({ column }) => <TanStackTableHeadSort column={column}>Response</TanStackTableHeadSort>,
sortingFn: (rowA, rowB, columnId) => {
const responseA = rowA.getValue<number | undefined>(columnId) ?? -1
const responseB = rowB.getValue<number | undefined>(columnId) ?? -1
return responseA - responseB
},
cell: ({ row }) =>
row.original.responseCode != null ? (
<DataTableColumnStatusCode
value={row.original.responseCode}
level={getStatusLevel(row.original.responseCode)}
className="text-xs"
/>
) : (
<span className="text-xs text-foreground-muted">–</span>
),
},
{
accessorKey: 'attemptAt',
header: ({ column }) => (
<TanStackTableHeadSort column={column}>Attempted</TanStackTableHeadSort>
),
cell: ({ row }) => (
<TimestampInfo
className="text-sm text-foreground-lighter"
utcTimestamp={row.original.attemptAt}
/>
),
},
{
id: DELIVERY_ACTIONS_COLUMN_ID,
enableSorting: false,
header: () => <span className="sr-only">Actions</span>,
cell: ({ row, table }) => {
const { onRetryDelivery } = table.options.meta as {
onRetryDelivery: (deliveryId: string) => void
}
return (
<div className="flex h-full items-center justify-end">
{row.original.status !== 'success' ? (
<ButtonTooltip
size="tiny"
className="w-7 shrink-0 hit-area-2"
icon={<RotateCcw />}
aria-label={`Retry ${row.original.id}`}
tooltip={{ content: { side: 'top', text: 'Retry' } }}
onClick={(event) => {
event.stopPropagation()
onRetryDelivery(row.original.id)
}}
onKeyDown={(event) => event.stopPropagation()}
/>
) : (
<Button
size="tiny"
className="w-7 shrink-0 hit-area-2 invisible pointer-events-none"
icon={<RotateCcw />}
aria-hidden
tabIndex={-1}
/>
)}
</div>
)
},
},
]
export const PlatformWebhooksEndpointDetails = ({
deliverySearch,
filteredDeliveries,
selectedEndpoint,
onCopyUrl,
onDeliverySearchChange,
onOpenDelivery,
onRetryDelivery,
}: PlatformWebhooksEndpointDetailsProps) => {
const hasCustomHeaders = selectedEndpoint.customHeaders.length > 0
const hasName = selectedEndpoint.name.trim().length > 0
const hasDescription = selectedEndpoint.description.trim().length > 0
const deliverySearchRef = useRef<HTMLInputElement>(null)
const [sorting, setSorting] = useState<SortingState>(DEFAULT_DELIVERY_SORTING)
useShortcut(SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => deliverySearchRef.current?.focus(), {
label: 'Search deliveries',
})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: DELIVERIES_PAGE_SIZE,
})
const table = useReactTable({
data: filteredDeliveries,
columns: DELIVERY_COLUMNS,
state: { pagination, sorting },
meta: { onRetryDelivery },
getRowId: (row) => row.id,
onPaginationChange: setPagination,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
const paginatedDeliveries = table.getRowModel().rows
const deliveryStartIndex =
table.getState().pagination.pageIndex * table.getState().pagination.pageSize
const deliveryRangeStart = filteredDeliveries.length === 0 ? 0 : deliveryStartIndex + 1
const deliveryRangeEnd = Math.min(
deliveryStartIndex + table.getState().pagination.pageSize,
filteredDeliveries.length
)
useEffect(() => {
setPagination((currentPagination) => ({ ...currentPagination, pageIndex: 0 }))
}, [deliverySearch, selectedEndpoint.id])
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="flex items-start gap-2 text-sm">
<span className="break-all">{selectedEndpoint.url}</span>
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL}
label="Copy endpoint URL"
>
<Button
variant="text"
size="tiny"
className="mt-0.5 shrink-0 h-5 w-5 p-0"
icon={<Copy size={12} />}
aria-label="Copy endpoint URL"
onClick={onCopyUrl}
/>
</ShortcutTooltip>
</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>{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
ref={deliverySearchRef}
placeholder="Search deliveries"
size="tiny"
icon={<Search />}
value={deliverySearch}
className="w-full lg:w-52"
onChange={(event) => onDeliverySearchChange(event.target.value)}
onKeyDown={onSearchInputEscape(deliverySearch, onDeliverySearchChange)}
/>
</div>
<Card className="overflow-hidden">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const columnId = header.column.id
const sort = header.column.getIsSorted()
return (
<TableHead
key={header.id}
aria-sort={
header.column.getCanSort()
? sort === 'asc'
? 'ascending'
: sort === 'desc'
? 'descending'
: 'none'
: undefined
}
className={columnId === DELIVERY_ACTIONS_COLUMN_ID ? 'w-1' : ''}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{paginatedDeliveries.length > 0 ? (
paginatedDeliveries.map((row) => (
<TableRow
key={row.id}
className="cursor-pointer focus-inset"
onClick={() => onOpenDelivery(row.original.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onOpenDelivery(row.original.id)
}
}}
tabIndex={0}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={
cell.column.id === DELIVERY_ACTIONS_COLUMN_ID ? 'w-1 text-right' : ''
}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow className="[&>td]:hover:bg-inherit">
<TableCell colSpan={DELIVERY_COLUMNS.length}>
<p className="text-sm text-foreground">No deliveries found</p>
<p className="text-sm text-foreground-lighter">
Try adjusting your search to see more webhook attempts.
</p>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{filteredDeliveries.length > 0 && (
<CardFooter className="border-t p-4 flex items-center justify-between">
<p className="text-foreground-muted text-sm">
Showing {deliveryRangeStart} to {deliveryRangeEnd} of {filteredDeliveries.length}{' '}
deliveries
</p>
<div className="flex items-center gap-x-2" aria-label="Pagination">
<Button
icon={<ChevronLeft />}
className="w-7 hit-area-2"
aria-label="Previous page"
size="tiny"
disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
/>
<Button
icon={<ChevronRight />}
className="w-7 hit-area-2"
aria-label="Next page"
size="tiny"
disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
/>
</div>
</CardFooter>
)}
</Card>
</div>
</div>
)
}