import { useState } from 'react' import { createFileRoute, Link } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Alert, Badge, Button, EmptyState, IconButton, LoadingBlock, PageHeader, Panel, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, } from '@nstart/ui' import { ArrowClockwiseIcon, ArrowsClockwiseIcon, KeyIcon, PaperPlaneTiltIcon, TrashIcon, } from '@phosphor-icons/react' import { useWebhookDeliveries, useWebhookSubscription, } from '@/api/queries' import { useDeleteWebhook, useRetryWebhookDelivery, useRotateWebhookSecret, useTestWebhook, } from '@/api/mutations' import type { WebhookTestPingResult } from '@/api/client' import { SCOPE_DOT } from '@/lib/scope-style' export const Route = createFileRoute('/webhooks/$id')({ component: WebhookDetailPage, }) const STATUS_BADGE: Record = { delivered: 'success', retrying: 'warning', pending: 'neutral', dlq: 'error', } function WebhookDetailPage() { const { id } = Route.useParams() const { t } = useTranslation() const sub = useWebhookSubscription(id) const [page, setPage] = useState(0) const [statusFilter, setStatusFilter] = useState(undefined) const deliveries = useWebhookDeliveries(id, page, 50, statusFilter) const handleStatusToggle = (s: string) => { setPage(0) setStatusFilter((curr) => (curr === s ? undefined : s)) } const rotateMut = useRotateWebhookSecret() const deleteMut = useDeleteWebhook() const retryMut = useRetryWebhookDelivery() const testMut = useTestWebhook() const [testResult, setTestResult] = useState(null) const handleTest = () => { setTestResult(null) testMut.mutate(id, { onSuccess: (r) => setTestResult(r), onError: (e) => setTestResult({ status: 'rejected', message: e instanceof Error ? e.message : String(e), }), }) } const handleRetry = (deliveryId: number) => { if (!window.confirm(t('webhooks.deliveries.confirmRetry'))) return retryMut.mutate(deliveryId) } const [revealedSecret, setRevealedSecret] = useState(null) const handleRotate = () => { if (!window.confirm(t('webhooks.confirmRotate'))) return rotateMut.mutate(id, { onSuccess: (s) => setRevealedSecret(s.hmacSecret), }) } const handleDelete = () => { if (!sub.data) return if (!window.confirm(t('webhooks.confirmDelete', { name: sub.data.name }))) return deleteMut.mutate(id, { onSuccess: () => { window.location.assign('/webhooks') }, }) } if (sub.isLoading) return if (sub.error || !sub.data) { return ( {String(sub.error ?? t('webhooks.notFound'))} ) } const data = sub.data return (
← {t('nav.webhooks')} } title={data.name} description={data.description ?? data.url} actions={
} /> {testResult && setTestResult(null)} />}
{data.url} {data.active ? t('webhooks.active') : t('webhooks.inactive')} {data.scopeFilter && data.scopeFilter.length > 0 ? (
{data.scopeFilter.map((s) => ( {s} ))}
) : ( {t('webhooks.allScopes')} )}
{data.dictionaryFilter && data.dictionaryFilter.length > 0 ? data.dictionaryFilter.join(', ') : t('webhooks.allDicts')} {data.eventTypeFilter && data.eventTypeFilter.length > 0 ? data.eventTypeFilter.join(', ') : t('webhooks.allEvents')} {data.hmacSecret} {data.createdBy} · {new Date(data.createdAt).toLocaleString()}

{t('webhooks.deliveries.title')}

} onClick={() => deliveries.refetch()} />
{(['pending', 'retrying', 'delivered', 'dlq'] as const).map((s) => { const active = statusFilter === s return ( ) })} {statusFilter && ( )}
{deliveries.isLoading ? ( ) : !deliveries.data || deliveries.data.content.length === 0 ? ( ) : ( {t('webhooks.deliveries.col.eventId')} {t('webhooks.col.status')} {t('webhooks.deliveries.col.attempts')} {t('webhooks.deliveries.col.lastAttempt')} {t('webhooks.deliveries.col.lastStatusCode')} {t('webhooks.deliveries.col.lastError')} {t('dict.col.actions')} {deliveries.data.content.map((d) => { const canRetry = d.status === 'dlq' || d.status === 'retrying' return ( {d.outboxEventId} {d.status} {d.attemptCount} {d.lastAttemptAt ? new Date(d.lastAttemptAt).toLocaleString() : '—'} {d.lastStatusCode ?? '—'} {d.lastError ?? '—'} {canRetry && ( } disabled={retryMut.isPending} onClick={() => handleRetry(d.id)} /> )} ) })}
)} {deliveries.data && (deliveries.data.totalPages ?? 0) > 1 && (
{page + 1} / {deliveries.data.totalPages}
)}
{revealedSecret && ( setRevealedSecret(null)} /> )}
) } function TestResultAlert({ result, onDismiss, }: { result: WebhookTestPingResult onDismiss: () => void }) { const { t } = useTranslation() const variant = result.status === 'success' ? 'success' : result.status === 'failure' ? 'error' : 'warning' const titleKey = result.status === 'success' ? 'webhooks.test.success' : result.status === 'failure' ? 'webhooks.test.failure' : 'webhooks.test.rejected' return ( ✕ } >
{result.httpStatus != null && (
HTTP {result.httpStatus}
)} {result.latencyMs != null && (
{t('webhooks.test.latency')} {result.latencyMs} ms
)} {result.message && (
{result.message}
)}
) } function InfoRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
) } function RotateRevealModal({ secret, onClose, }: { secret: string onClose: () => void }) { const { t } = useTranslation() return (
e.stopPropagation()} >

{t('webhooks.secret.rotatedTitle')}

{t('webhooks.secret.rotatedWarning')}
{secret}
) }