import { useRouter } from 'next/router' import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { EllipsisVertical, Pencil, RotateCw, Trash2 } from 'lucide-react' import { useParams } from 'common' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { InlineLink } from 'components/ui/InlineLink' import { Admonition } from 'ui-patterns' import { useIsPlatformWebhooksEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Label_Shadcn_, copyToClipboard, } from 'ui' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' import { Input } from 'ui-patterns/DataInputs/Input' import { PLATFORM_WEBHOOKS_MOCK_DATA } from './PlatformWebhooks.mock' import { PlatformWebhooksDeliveryDetailsSheet } from './PlatformWebhooksDeliveryDetailsSheet' import { PlatformWebhooksEndpointDetails } from './PlatformWebhooksEndpointDetails' import { PlatformWebhooksEndpointList } from './PlatformWebhooksEndpointList' import { EndpointFormValues, PlatformWebhooksEndpointSheet, toEndpointPayload, } from './PlatformWebhooksEndpointSheet' import { clearPendingSigningSecretReveal, getPendingSigningSecretReveal, setPendingSigningSecretReveal, shouldHandleEndpointNotFound, } from './PlatformWebhooksPage.utils' import { PlatformWebhooksHeader } from './PlatformWebhooksHeader' import { filterWebhookDeliveries, filterWebhookEndpoints, usePlatformWebhooksMockStore, } from './PlatformWebhooks.store' import type { WebhookScope } from './PlatformWebhooks.types' 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, } = 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(null) const [signingSecretReveal, setSigningSecretReveal] = useState<{ signingSecret: string } | null>( null ) const [showRegenerateSecretConfirm, setShowRegenerateSecretConfirm] = useState(false) const [editEnabledOverride, setEditEnabledOverride] = useState(null) const [deliveryDetailsTab, setDeliveryDetailsTab] = useState<'event' | 'response'>('event') const [pendingCreatedEndpointId, setPendingCreatedEndpointId] = useState(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 headerTitle = isEndpointView ? 'Endpoint' : scopeLabel const headerDescription = isEndpointView ? selectedEndpoint?.url ?? '' : scopeDescription const endpointPendingDelete = useMemo( () => endpoints.find((endpoint) => endpoint.id === endpointIdPendingDelete) ?? null, [endpoints, endpointIdPendingDelete] ) 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(`Deleted endpoint "${endpointPendingDelete.url}"`) } 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 handleCopy = (value: string, label: string) => { copyToClipboard(value) toast.success(`Copied ${label}`) } const isEndpointSheetOpen = panel === 'create' || (panel === 'edit' && !!selectedEndpoint) useEffect(() => { if (!selectedEndpoint && !!deliveryId) { setDeliveryId(null) } }, [deliveryId, selectedEndpoint, setDeliveryId]) useEffect(() => { if (!!deliveryId && !selectedDelivery) { setDeliveryId(null) } }, [deliveryId, selectedDelivery, setDeliveryId]) if (!platformWebhooksEnabled) { return null } return ( <>