Files
Alaister YoungandAlaister Young ca2b50a0a7 chore(ui-patterns): collapse the admonition shim into ui-patterns/Admonition (#48377)
Follow-up to #48344: collapses the two resolution paths for the
Admonition module into one.

`src/admonition.tsx` was a back-compat shim re-exporting
`src/Admonition/`. Two ways to resolve one module is exactly what
produced the macOS self-import bug fixed in #48344, and the local
typecheck errors that #48374 worked around. This removes the shim and
standardizes on the PascalCase subpath, matching every other export in
the package.

**Changed:**

- Codemodded all 246 `ui-patterns/admonition` imports to
`ui-patterns/Admonition` (240 `.tsx`, 5 `.mdx`, 1 `.ts` across studio,
docs, www, design-system, and lite-studio)
- Pointed the 5 internal `'../admonition'` imports back at the
`'../Admonition'` directory

**Removed:**

- `packages/ui-patterns/src/admonition.tsx`, and its `./admonition`
entry in the exports map (regenerated with `pnpm gen:exports`)

## To test

- `grep -r "ui-patterns/admonition" --include='*.ts*'` → no hits
- `pnpm test:case-hazards` → passes
- `pnpm typecheck` → all 15 tasks green
- `pnpm --filter studio run lint:ratchet` → passes
- `pnpm --filter ui-patterns vitest run src/Admonition` → 11 tests pass

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Standardized Admonition component imports across the application and
documentation.
* Improved compatibility with case-sensitive environments by using the
canonical component path.
  * Removed the legacy Admonition import entry point.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-07-29 00:48:56 +08:00

537 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
variant="default"
icon={<Pencil size={14} />}
onClick={() => {
setEditEnabledOverride(null)
setPanel('edit')
}}
>
Edit
</Button>
</Shortcut>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" 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>
</>
)
}