Files
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

536 lines
19 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 { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { EllipsisVertical, Pencil, RotateCw, Trash2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Label,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { Input } from 'ui-patterns/DataInputs/Input'
import { PageContainer } from 'ui-patterns/PageContainer'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { PLATFORM_WEBHOOKS_MOCK_DATA } from './PlatformWebhooks.mock'
import {
filterWebhookDeliveries,
filterWebhookEndpoints,
usePlatformWebhooksMockStore,
} from './PlatformWebhooks.store'
import type { WebhookScope } from './PlatformWebhooks.types'
import { getWebhookEndpointDisplayName } from './PlatformWebhooks.utils'
import { PlatformWebhooksDeliveryDetailsSheet } from './PlatformWebhooksDeliveryDetailsSheet'
import { PlatformWebhooksEndpointDetails } from './PlatformWebhooksEndpointDetails'
import { PlatformWebhooksEndpointList } from './PlatformWebhooksEndpointList'
import {
EndpointFormValues,
PlatformWebhooksEndpointSheet,
toEndpointPayload,
} from './PlatformWebhooksEndpointSheet'
import { PlatformWebhooksHeader } from './PlatformWebhooksHeader'
import {
clearPendingSigningSecretReveal,
getPendingSigningSecretReveal,
setPendingSigningSecretReveal,
shouldHandleEndpointNotFound,
} from './PlatformWebhooksPage.utils'
import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { InlineLink } from '@/components/ui/InlineLink'
import { Shortcut } from '@/components/ui/Shortcut'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
const PANEL_VALUES = ['create', 'edit'] as const
interface PlatformWebhooksPageProps {
scope: WebhookScope
endpointId?: string
}
export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPageProps) => {
const router = useRouter()
const { slug, ref } = useParams()
const { data: selectedOrganization } = useSelectedOrganizationQuery({
enabled: scope === 'project',
})
const platformWebhooksEnabled = useIsPlatformWebhooksEnabled()
const {
endpoints,
deliveries,
createEndpoint,
updateEndpoint,
deleteEndpoint,
regenerateSecret,
retryDelivery,
} = usePlatformWebhooksMockStore(scope)
const [deliveryId, setDeliveryId] = useQueryState('deliveryId', parseAsString)
const [panel, setPanel] = useQueryState('panel', parseAsStringLiteral(PANEL_VALUES))
const [search, setSearch] = useQueryState(
'search',
parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true })
)
const [deliverySearch, setDeliverySearch] = useQueryState(
'deliverySearch',
parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true })
)
const [endpointIdPendingDelete, setEndpointIdPendingDelete] = useState<string | null>(null)
const [signingSecretReveal, setSigningSecretReveal] = useState<{ signingSecret: string } | null>(
null
)
const [showRegenerateSecretConfirm, setShowRegenerateSecretConfirm] = useState(false)
const [editEnabledOverride, setEditEnabledOverride] = useState<boolean | null>(null)
const [deliveryDetailsTab, setDeliveryDetailsTab] = useState<'event' | 'response'>('event')
const [pendingCreatedEndpointId, setPendingCreatedEndpointId] = useState<string | null>(null)
const scopeLabel = scope === 'organization' ? 'Organization Webhooks' : 'Project Webhooks'
const scopeDescription =
scope === 'organization'
? 'Organization-level webhook endpoints and deliveries'
: 'Webhook endpoints specific to this project'
const fallbackHref =
scope === 'organization' ? `/org/${slug}/general` : `/project/${ref}/settings/general`
const eventTypeOptions = PLATFORM_WEBHOOKS_MOCK_DATA[scope].eventTypes
const webhooksHref =
scope === 'organization' ? `/org/${slug}/webhooks` : `/project/${ref}/settings/webhooks`
const selectedEndpoint = useMemo(
() => endpoints.find((endpoint) => endpoint.id === endpointId) ?? null,
[endpoints, endpointId]
)
const isEndpointView = !!selectedEndpoint
const selectedEndpointHasName = selectedEndpoint ? selectedEndpoint.name.trim().length > 0 : false
const selectedEndpointDisplayName = selectedEndpoint
? getWebhookEndpointDisplayName(selectedEndpoint)
: ''
const headerTitle = isEndpointView ? selectedEndpointDisplayName : scopeLabel
const headerDescription = isEndpointView
? selectedEndpointHasName
? (selectedEndpoint?.url ?? '')
: ''
: scopeDescription
const endpointPendingDelete = useMemo(
() => 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) {
router.replace(fallbackHref)
}
}, [fallbackHref, platformWebhooksEnabled, router])
useEffect(() => {
if (
shouldHandleEndpointNotFound({
endpointId,
hasSelectedEndpoint: !!selectedEndpoint,
pendingCreatedEndpointId,
})
) {
toast('Endpoint not found')
router.replace(webhooksHref)
}
}, [endpointId, pendingCreatedEndpointId, selectedEndpoint, router, webhooksHref])
useEffect(() => {
if (!pendingCreatedEndpointId) return
if (
endpointId !== pendingCreatedEndpointId ||
selectedEndpoint?.id === pendingCreatedEndpointId
) {
setPendingCreatedEndpointId(null)
}
}, [endpointId, pendingCreatedEndpointId, selectedEndpoint])
useEffect(() => {
if (signingSecretReveal || !endpointId) return
const pendingReveal = getPendingSigningSecretReveal(scope, endpointId)
if (!pendingReveal) return
setSigningSecretReveal({ signingSecret: pendingReveal.signingSecret })
}, [endpointId, scope, signingSecretReveal])
const filteredEndpoints = useMemo(() => {
return filterWebhookEndpoints(endpoints, search)
}, [endpoints, search])
const filteredDeliveries = useMemo(() => {
if (!selectedEndpoint) return []
return filterWebhookDeliveries(deliveries, selectedEndpoint.id, deliverySearch)
}, [deliveries, deliverySearch, selectedEndpoint])
const selectedDelivery = useMemo(() => {
if (!selectedEndpoint || !deliveryId) return null
return (
deliveries.find(
(delivery) => delivery.id === deliveryId && delivery.endpointId === selectedEndpoint.id
) ?? null
)
}, [deliveries, deliveryId, selectedEndpoint])
const deliveryAttempt = useMemo(() => {
if (!selectedEndpoint || !selectedDelivery) return null
const endpointDeliveries = deliveries
.filter((delivery) => delivery.endpointId === selectedEndpoint.id)
.sort((a, b) => new Date(b.attemptAt).getTime() - new Date(a.attemptAt).getTime())
const index = endpointDeliveries.findIndex((delivery) => delivery.id === selectedDelivery.id)
return index >= 0 ? index + 1 : null
}, [deliveries, selectedDelivery, selectedEndpoint])
const deliveryEventPayload = useMemo(() => {
if (!selectedEndpoint || !selectedDelivery) return ''
return JSON.stringify(
{
endpoint_id: selectedEndpoint.id,
endpoint_url: selectedEndpoint.url,
event_type: selectedDelivery.eventType,
event_id: selectedDelivery.id,
attempted_at: selectedDelivery.attemptAt,
scope,
},
null,
2
)
}, [scope, selectedDelivery, selectedEndpoint])
const deliveryResponsePayload = useMemo(() => {
if (!selectedEndpoint || !selectedDelivery) return ''
return JSON.stringify(
{
endpoint_id: selectedEndpoint.id,
delivery_id: selectedDelivery.id,
status: selectedDelivery.status,
response_code: selectedDelivery.responseCode ?? null,
},
null,
2
)
}, [selectedDelivery, selectedEndpoint])
const handleDeleteEndpoint = () => {
if (!endpointPendingDelete) return
deleteEndpoint(endpointPendingDelete.id)
if (endpointPendingDelete.id === endpointId) {
router.push(webhooksHref)
setDeliverySearch('')
}
setEndpointIdPendingDelete(null)
toast.success('Endpoint deleted')
}
const handleUpsertEndpoint = (values: EndpointFormValues) => {
if (panel === 'create') {
const { endpointId: createdEndpointId, signingSecret } = createEndpoint(
toEndpointPayload(values)
)
setPendingCreatedEndpointId(createdEndpointId)
setPendingSigningSecretReveal(scope, {
endpointId: createdEndpointId,
signingSecret,
})
router.push(`${webhooksHref}/${encodeURIComponent(createdEndpointId)}`)
setSigningSecretReveal({ signingSecret })
setPanel(null)
setEditEnabledOverride(null)
toast.success('Endpoint created')
return
}
if (panel === 'edit' && selectedEndpoint) {
updateEndpoint(selectedEndpoint.id, toEndpointPayload(values))
setPanel(null)
setEditEnabledOverride(null)
toast.success('Endpoint updated')
}
}
const handleRegenerateSecret = () => {
if (!selectedEndpoint) return
const nextSecret = regenerateSecret(selectedEndpoint.id)
if (!nextSecret) return
setSigningSecretReveal({ signingSecret: nextSecret })
setShowRegenerateSecretConfirm(false)
toast.success('Signing secret regenerated')
}
const handleRetryDelivery = (deliveryId: string) => {
const delivery = deliveries.find((item) => item.id === deliveryId)
if (!delivery || delivery.status === 'success') return
retryDelivery(deliveryId)
toast.success('Delivery queued for retry')
}
const handleCopy = (value: string, label: string) => {
copyToClipboard(value)
toast.success(`Copied ${label}`)
}
const isEndpointSheetOpen = panel === 'create' || (panel === 'edit' && !!selectedEndpoint)
useShortcut(
SHORTCUT_IDS.PLATFORM_WEBHOOKS_COPY_ENDPOINT_URL,
() => selectedEndpoint && handleCopy(selectedEndpoint.url, 'endpoint URL'),
{ enabled: isEndpointView && !isEndpointSheetOpen }
)
useEffect(() => {
if (!selectedEndpoint && !!deliveryId) {
setDeliveryId(null)
}
}, [deliveryId, selectedEndpoint, setDeliveryId])
useEffect(() => {
if (!!deliveryId && !selectedDelivery) {
setDeliveryId(null)
}
}, [deliveryId, selectedDelivery, setDeliveryId])
if (!platformWebhooksEnabled) {
return null
}
return (
<>
<PlatformWebhooksHeader
hasSelectedEndpoint={!!selectedEndpoint}
headerTitle={headerTitle}
featureKey={LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS}
headerDescription={headerDescription}
endpointStatus={
selectedEndpoint ? (selectedEndpoint.enabled ? 'enabled' : 'disabled') : undefined
}
endpointActions={
selectedEndpoint ? (
<>
<Shortcut
id={SHORTCUT_IDS.PLATFORM_WEBHOOKS_EDIT_ENDPOINT}
onTrigger={() => {
setEditEnabledOverride(null)
setPanel('edit')
}}
options={{ enabled: !isEndpointSheetOpen }}
>
<Button
icon={<Pencil size={14} />}
onClick={() => {
setEditEnabledOverride(null)
setPanel('edit')
}}
>
Edit
</Button>
</Shortcut>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={<EllipsisVertical />} className="w-7" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className="w-48">
<DropdownMenuItem
className="gap-x-2"
onClick={() => setShowRegenerateSecretConfirm(true)}
>
<RotateCw size={14} className="text-foreground-lighter" />
<span>Regenerate secret</span>
</DropdownMenuItem>
<DropdownMenuItem
className="gap-x-2"
onClick={() => setEndpointIdPendingDelete(selectedEndpoint.id)}
>
<Trash2 size={14} className="text-foreground-lighter" />
<span>Delete endpoint</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
) : undefined
}
webhooksHref={webhooksHref}
scopeLabel={scopeLabel}
/>
<PageContainer size="default">
<PageSection>
<PageSectionContent>
{!selectedEndpoint ? (
<PlatformWebhooksEndpointList
filteredEndpoints={filteredEndpoints}
search={search}
webhooksHref={webhooksHref}
onCreateEndpoint={() => setPanel('create')}
onDeleteEndpoint={(id) => setEndpointIdPendingDelete(id)}
onSearchChange={setSearch}
onViewEndpoint={(id) => {
router.push(`${webhooksHref}/${encodeURIComponent(id)}`)
setPanel(null)
}}
/>
) : (
<PlatformWebhooksEndpointDetails
deliverySearch={deliverySearch}
filteredDeliveries={filteredDeliveries}
selectedEndpoint={selectedEndpoint}
onCopyUrl={() => handleCopy(selectedEndpoint.url, 'endpoint URL')}
onDeliverySearchChange={setDeliverySearch}
onOpenDelivery={(id) => {
setDeliveryDetailsTab('event')
setDeliveryId(id)
}}
onRetryDelivery={handleRetryDelivery}
/>
)}
</PageSectionContent>
</PageSection>
</PageContainer>
<PlatformWebhooksDeliveryDetailsSheet
deliveryAttempt={deliveryAttempt}
deliveryDetailsTab={deliveryDetailsTab}
deliveryEventPayload={deliveryEventPayload}
deliveryResponsePayload={deliveryResponsePayload}
open={!!selectedDelivery}
selectedDelivery={selectedDelivery}
onCopy={handleCopy}
onOpenChange={(open) => !open && setDeliveryId(null)}
onRetryDelivery={handleRetryDelivery}
onTabChange={setDeliveryDetailsTab}
/>
<PlatformWebhooksEndpointSheet
visible={isEndpointSheetOpen}
mode={panel === 'create' ? 'create' : 'edit'}
scope={scope}
orgSlug={scope === 'project' ? selectedOrganization?.slug : undefined}
endpoint={panel === 'edit' ? (selectedEndpoint ?? undefined) : undefined}
enabledOverride={panel === 'edit' ? editEnabledOverride : null}
eventTypes={eventTypeOptions}
onClose={() => {
setPanel(null)
setEditEnabledOverride(null)
}}
onSubmit={handleUpsertEndpoint}
/>
<AlertDialog
open={!!endpointPendingDelete}
onOpenChange={(open) => !open && setEndpointIdPendingDelete(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete endpoint</AlertDialogTitle>
<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}>
Delete endpoint
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={showRegenerateSecretConfirm} onOpenChange={setShowRegenerateSecretConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Regenerate secret</AlertDialogTitle>
<AlertDialogDescription>
This will rotate the current signing secret used for webhook signature verification.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="warning" onClick={handleRegenerateSecret}>
Regenerate
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={!!signingSecretReveal}
onOpenChange={(open) => {
if (open) return
setSigningSecretReveal(null)
clearPendingSigningSecretReveal(scope)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Signing secret</AlertDialogTitle>
<AlertDialogDescription>
Use this secret to verify webhook signatures using the{' '}
<InlineLink href="https://www.standardwebhooks.com/">Standard Webhooks</InlineLink>{' '}
specification.
</AlertDialogDescription>
</AlertDialogHeader>
{/* Content */}
<div className="space-y-4 mx-5 pb-5">
<div className="space-y-1">
<Label>Signing secret</Label>
<Input
copy
readOnly
value={signingSecretReveal?.signingSecret ?? ''}
onChange={() => {}}
onCopy={() => toast.success('Copied signing secret')}
/>
</div>
<div>
<Admonition
type="warning"
title="This secret won’t be shown again"
description="Copy and store it securely now. You will not be able to view or copy it again after closing this dialog."
/>
</div>
</div>
<AlertDialogFooter>
<AlertDialogAction
onClick={() => {
setSigningSecretReveal(null)
clearPendingSigningSecretReveal(scope)
}}
>
I’ve stored the secret
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}