PlatformWebhooksPage.tsx517 lines · main
| 1 | import { LOCAL_STORAGE_KEYS, useParams } from 'common' |
| 2 | import { EllipsisVertical, Pencil, RotateCw, Trash2 } from 'lucide-react' |
| 3 | import { useRouter } from 'next/router' |
| 4 | import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs' |
| 5 | import { useEffect, useMemo, useState } from 'react' |
| 6 | import { toast } from 'sonner' |
| 7 | import { |
| 8 | AlertDialog, |
| 9 | AlertDialogAction, |
| 10 | AlertDialogCancel, |
| 11 | AlertDialogContent, |
| 12 | AlertDialogDescription, |
| 13 | AlertDialogFooter, |
| 14 | AlertDialogHeader, |
| 15 | AlertDialogTitle, |
| 16 | Button, |
| 17 | copyToClipboard, |
| 18 | DropdownMenu, |
| 19 | DropdownMenuContent, |
| 20 | DropdownMenuItem, |
| 21 | DropdownMenuTrigger, |
| 22 | Label, |
| 23 | } from 'ui' |
| 24 | import { Admonition } from 'ui-patterns' |
| 25 | import { Input } from 'ui-patterns/DataInputs/Input' |
| 26 | import { PageContainer } from 'ui-patterns/PageContainer' |
| 27 | import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' |
| 28 | |
| 29 | import { PLATFORM_WEBHOOKS_MOCK_DATA } from './PlatformWebhooks.mock' |
| 30 | import { |
| 31 | filterWebhookDeliveries, |
| 32 | filterWebhookEndpoints, |
| 33 | usePlatformWebhooksMockStore, |
| 34 | } from './PlatformWebhooks.store' |
| 35 | import type { WebhookScope } from './PlatformWebhooks.types' |
| 36 | import { getWebhookEndpointDisplayName } from './PlatformWebhooks.utils' |
| 37 | import { PlatformWebhooksDeliveryDetailsSheet } from './PlatformWebhooksDeliveryDetailsSheet' |
| 38 | import { PlatformWebhooksEndpointDetails } from './PlatformWebhooksEndpointDetails' |
| 39 | import { PlatformWebhooksEndpointList } from './PlatformWebhooksEndpointList' |
| 40 | import { |
| 41 | EndpointFormValues, |
| 42 | PlatformWebhooksEndpointSheet, |
| 43 | toEndpointPayload, |
| 44 | } from './PlatformWebhooksEndpointSheet' |
| 45 | import { PlatformWebhooksHeader } from './PlatformWebhooksHeader' |
| 46 | import { |
| 47 | clearPendingSigningSecretReveal, |
| 48 | getPendingSigningSecretReveal, |
| 49 | setPendingSigningSecretReveal, |
| 50 | shouldHandleEndpointNotFound, |
| 51 | } from './PlatformWebhooksPage.utils' |
| 52 | import { useIsPlatformWebhooksEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' |
| 53 | import { InlineLink } from '@/components/ui/InlineLink' |
| 54 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 55 | |
| 56 | const PANEL_VALUES = ['create', 'edit'] as const |
| 57 | |
| 58 | interface PlatformWebhooksPageProps { |
| 59 | scope: WebhookScope |
| 60 | endpointId?: string |
| 61 | } |
| 62 | |
| 63 | export const PlatformWebhooksPage = ({ scope, endpointId }: PlatformWebhooksPageProps) => { |
| 64 | const router = useRouter() |
| 65 | const { slug, ref } = useParams() |
| 66 | const { data: selectedOrganization } = useSelectedOrganizationQuery({ |
| 67 | enabled: scope === 'project', |
| 68 | }) |
| 69 | const platformWebhooksEnabled = useIsPlatformWebhooksEnabled() |
| 70 | const { |
| 71 | endpoints, |
| 72 | deliveries, |
| 73 | createEndpoint, |
| 74 | updateEndpoint, |
| 75 | deleteEndpoint, |
| 76 | regenerateSecret, |
| 77 | retryDelivery, |
| 78 | } = usePlatformWebhooksMockStore(scope) |
| 79 | const [deliveryId, setDeliveryId] = useQueryState('deliveryId', parseAsString) |
| 80 | const [panel, setPanel] = useQueryState('panel', parseAsStringLiteral(PANEL_VALUES)) |
| 81 | const [search, setSearch] = useQueryState( |
| 82 | 'search', |
| 83 | parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true }) |
| 84 | ) |
| 85 | const [deliverySearch, setDeliverySearch] = useQueryState( |
| 86 | 'deliverySearch', |
| 87 | parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true }) |
| 88 | ) |
| 89 | |
| 90 | const [endpointIdPendingDelete, setEndpointIdPendingDelete] = useState<string | null>(null) |
| 91 | const [signingSecretReveal, setSigningSecretReveal] = useState<{ signingSecret: string } | null>( |
| 92 | null |
| 93 | ) |
| 94 | const [showRegenerateSecretConfirm, setShowRegenerateSecretConfirm] = useState(false) |
| 95 | const [editEnabledOverride, setEditEnabledOverride] = useState<boolean | null>(null) |
| 96 | const [deliveryDetailsTab, setDeliveryDetailsTab] = useState<'event' | 'response'>('event') |
| 97 | const [pendingCreatedEndpointId, setPendingCreatedEndpointId] = useState<string | null>(null) |
| 98 | |
| 99 | const scopeLabel = scope === 'organization' ? 'Organization Webhooks' : 'Project Webhooks' |
| 100 | const scopeDescription = |
| 101 | scope === 'organization' |
| 102 | ? 'Organization-level webhook endpoints and deliveries' |
| 103 | : 'Webhook endpoints specific to this project' |
| 104 | const fallbackHref = |
| 105 | scope === 'organization' ? `/org/${slug}/general` : `/project/${ref}/settings/general` |
| 106 | |
| 107 | const eventTypeOptions = PLATFORM_WEBHOOKS_MOCK_DATA[scope].eventTypes |
| 108 | const webhooksHref = |
| 109 | scope === 'organization' ? `/org/${slug}/webhooks` : `/project/${ref}/settings/webhooks` |
| 110 | |
| 111 | const selectedEndpoint = useMemo( |
| 112 | () => endpoints.find((endpoint) => endpoint.id === endpointId) ?? null, |
| 113 | [endpoints, endpointId] |
| 114 | ) |
| 115 | const isEndpointView = !!selectedEndpoint |
| 116 | const selectedEndpointHasName = selectedEndpoint ? selectedEndpoint.name.trim().length > 0 : false |
| 117 | const selectedEndpointDisplayName = selectedEndpoint |
| 118 | ? getWebhookEndpointDisplayName(selectedEndpoint) |
| 119 | : '' |
| 120 | const headerTitle = isEndpointView ? selectedEndpointDisplayName : scopeLabel |
| 121 | const headerDescription = isEndpointView |
| 122 | ? selectedEndpointHasName |
| 123 | ? (selectedEndpoint?.url ?? '') |
| 124 | : '' |
| 125 | : scopeDescription |
| 126 | |
| 127 | const endpointPendingDelete = useMemo( |
| 128 | () => endpoints.find((endpoint) => endpoint.id === endpointIdPendingDelete) ?? null, |
| 129 | [endpoints, endpointIdPendingDelete] |
| 130 | ) |
| 131 | const endpointPendingDeleteHasName = endpointPendingDelete |
| 132 | ? endpointPendingDelete.name.trim().length > 0 |
| 133 | : false |
| 134 | const endpointPendingDeleteDisplayName = endpointPendingDelete |
| 135 | ? getWebhookEndpointDisplayName(endpointPendingDelete) |
| 136 | : '' |
| 137 | let deleteEndpointDescription = 'This action cannot be undone.' |
| 138 | if (endpointPendingDelete) { |
| 139 | deleteEndpointDescription = endpointPendingDeleteHasName |
| 140 | ? `Deleting “${endpointPendingDeleteDisplayName}” stops all deliveries to the URL below. This can’t be undone.` |
| 141 | : 'Deleting this endpoint stops all deliveries to the URL below. This can’t be undone.' |
| 142 | } |
| 143 | |
| 144 | useEffect(() => { |
| 145 | if (!platformWebhooksEnabled) { |
| 146 | router.replace(fallbackHref) |
| 147 | } |
| 148 | }, [fallbackHref, platformWebhooksEnabled, router]) |
| 149 | |
| 150 | useEffect(() => { |
| 151 | if ( |
| 152 | shouldHandleEndpointNotFound({ |
| 153 | endpointId, |
| 154 | hasSelectedEndpoint: !!selectedEndpoint, |
| 155 | pendingCreatedEndpointId, |
| 156 | }) |
| 157 | ) { |
| 158 | toast('Endpoint not found') |
| 159 | router.replace(webhooksHref) |
| 160 | } |
| 161 | }, [endpointId, pendingCreatedEndpointId, selectedEndpoint, router, webhooksHref]) |
| 162 | |
| 163 | useEffect(() => { |
| 164 | if (!pendingCreatedEndpointId) return |
| 165 | if ( |
| 166 | endpointId !== pendingCreatedEndpointId || |
| 167 | selectedEndpoint?.id === pendingCreatedEndpointId |
| 168 | ) { |
| 169 | setPendingCreatedEndpointId(null) |
| 170 | } |
| 171 | }, [endpointId, pendingCreatedEndpointId, selectedEndpoint]) |
| 172 | |
| 173 | useEffect(() => { |
| 174 | if (signingSecretReveal || !endpointId) return |
| 175 | const pendingReveal = getPendingSigningSecretReveal(scope, endpointId) |
| 176 | if (!pendingReveal) return |
| 177 | setSigningSecretReveal({ signingSecret: pendingReveal.signingSecret }) |
| 178 | }, [endpointId, scope, signingSecretReveal]) |
| 179 | |
| 180 | const filteredEndpoints = useMemo(() => { |
| 181 | return filterWebhookEndpoints(endpoints, search) |
| 182 | }, [endpoints, search]) |
| 183 | |
| 184 | const filteredDeliveries = useMemo(() => { |
| 185 | if (!selectedEndpoint) return [] |
| 186 | return filterWebhookDeliveries(deliveries, selectedEndpoint.id, deliverySearch) |
| 187 | }, [deliveries, deliverySearch, selectedEndpoint]) |
| 188 | |
| 189 | const selectedDelivery = useMemo(() => { |
| 190 | if (!selectedEndpoint || !deliveryId) return null |
| 191 | return ( |
| 192 | deliveries.find( |
| 193 | (delivery) => delivery.id === deliveryId && delivery.endpointId === selectedEndpoint.id |
| 194 | ) ?? null |
| 195 | ) |
| 196 | }, [deliveries, deliveryId, selectedEndpoint]) |
| 197 | |
| 198 | const deliveryAttempt = useMemo(() => { |
| 199 | if (!selectedEndpoint || !selectedDelivery) return null |
| 200 | const endpointDeliveries = deliveries |
| 201 | .filter((delivery) => delivery.endpointId === selectedEndpoint.id) |
| 202 | .sort((a, b) => new Date(b.attemptAt).getTime() - new Date(a.attemptAt).getTime()) |
| 203 | const index = endpointDeliveries.findIndex((delivery) => delivery.id === selectedDelivery.id) |
| 204 | return index >= 0 ? index + 1 : null |
| 205 | }, [deliveries, selectedDelivery, selectedEndpoint]) |
| 206 | |
| 207 | const deliveryEventPayload = useMemo(() => { |
| 208 | if (!selectedEndpoint || !selectedDelivery) return '' |
| 209 | return JSON.stringify( |
| 210 | { |
| 211 | endpoint_id: selectedEndpoint.id, |
| 212 | endpoint_url: selectedEndpoint.url, |
| 213 | event_type: selectedDelivery.eventType, |
| 214 | event_id: selectedDelivery.id, |
| 215 | attempted_at: selectedDelivery.attemptAt, |
| 216 | scope, |
| 217 | }, |
| 218 | null, |
| 219 | 2 |
| 220 | ) |
| 221 | }, [scope, selectedDelivery, selectedEndpoint]) |
| 222 | |
| 223 | const deliveryResponsePayload = useMemo(() => { |
| 224 | if (!selectedEndpoint || !selectedDelivery) return '' |
| 225 | return JSON.stringify( |
| 226 | { |
| 227 | endpoint_id: selectedEndpoint.id, |
| 228 | delivery_id: selectedDelivery.id, |
| 229 | status: selectedDelivery.status, |
| 230 | response_code: selectedDelivery.responseCode ?? null, |
| 231 | }, |
| 232 | null, |
| 233 | 2 |
| 234 | ) |
| 235 | }, [selectedDelivery, selectedEndpoint]) |
| 236 | |
| 237 | const handleDeleteEndpoint = () => { |
| 238 | if (!endpointPendingDelete) return |
| 239 | deleteEndpoint(endpointPendingDelete.id) |
| 240 | if (endpointPendingDelete.id === endpointId) { |
| 241 | router.push(webhooksHref) |
| 242 | setDeliverySearch('') |
| 243 | } |
| 244 | setEndpointIdPendingDelete(null) |
| 245 | toast.success('Endpoint deleted') |
| 246 | } |
| 247 | |
| 248 | const handleUpsertEndpoint = (values: EndpointFormValues) => { |
| 249 | if (panel === 'create') { |
| 250 | const { endpointId: createdEndpointId, signingSecret } = createEndpoint( |
| 251 | toEndpointPayload(values) |
| 252 | ) |
| 253 | setPendingCreatedEndpointId(createdEndpointId) |
| 254 | setPendingSigningSecretReveal(scope, { |
| 255 | endpointId: createdEndpointId, |
| 256 | signingSecret, |
| 257 | }) |
| 258 | router.push(`${webhooksHref}/${encodeURIComponent(createdEndpointId)}`) |
| 259 | setSigningSecretReveal({ signingSecret }) |
| 260 | setPanel(null) |
| 261 | setEditEnabledOverride(null) |
| 262 | toast.success('Endpoint created') |
| 263 | return |
| 264 | } |
| 265 | |
| 266 | if (panel === 'edit' && selectedEndpoint) { |
| 267 | updateEndpoint(selectedEndpoint.id, toEndpointPayload(values)) |
| 268 | setPanel(null) |
| 269 | setEditEnabledOverride(null) |
| 270 | toast.success('Endpoint updated') |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | const handleRegenerateSecret = () => { |
| 275 | if (!selectedEndpoint) return |
| 276 | const nextSecret = regenerateSecret(selectedEndpoint.id) |
| 277 | if (!nextSecret) return |
| 278 | setSigningSecretReveal({ signingSecret: nextSecret }) |
| 279 | setShowRegenerateSecretConfirm(false) |
| 280 | toast.success('Signing secret regenerated') |
| 281 | } |
| 282 | |
| 283 | const handleRetryDelivery = (deliveryId: string) => { |
| 284 | const delivery = deliveries.find((item) => item.id === deliveryId) |
| 285 | if (!delivery || delivery.status === 'success') return |
| 286 | |
| 287 | retryDelivery(deliveryId) |
| 288 | toast.success('Delivery queued for retry') |
| 289 | } |
| 290 | |
| 291 | const handleCopy = (value: string, label: string) => { |
| 292 | copyToClipboard(value) |
| 293 | toast.success(`Copied ${label}`) |
| 294 | } |
| 295 | |
| 296 | const isEndpointSheetOpen = panel === 'create' || (panel === 'edit' && !!selectedEndpoint) |
| 297 | |
| 298 | useEffect(() => { |
| 299 | if (!selectedEndpoint && !!deliveryId) { |
| 300 | setDeliveryId(null) |
| 301 | } |
| 302 | }, [deliveryId, selectedEndpoint, setDeliveryId]) |
| 303 | |
| 304 | useEffect(() => { |
| 305 | if (!!deliveryId && !selectedDelivery) { |
| 306 | setDeliveryId(null) |
| 307 | } |
| 308 | }, [deliveryId, selectedDelivery, setDeliveryId]) |
| 309 | |
| 310 | if (!platformWebhooksEnabled) { |
| 311 | return null |
| 312 | } |
| 313 | |
| 314 | return ( |
| 315 | <> |
| 316 | <PlatformWebhooksHeader |
| 317 | hasSelectedEndpoint={!!selectedEndpoint} |
| 318 | headerTitle={headerTitle} |
| 319 | featureKey={LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS} |
| 320 | headerDescription={headerDescription} |
| 321 | endpointStatus={ |
| 322 | selectedEndpoint ? (selectedEndpoint.enabled ? 'enabled' : 'disabled') : undefined |
| 323 | } |
| 324 | endpointActions={ |
| 325 | selectedEndpoint ? ( |
| 326 | <> |
| 327 | <Button |
| 328 | type="default" |
| 329 | icon={<Pencil size={14} />} |
| 330 | onClick={() => { |
| 331 | setEditEnabledOverride(null) |
| 332 | setPanel('edit') |
| 333 | }} |
| 334 | > |
| 335 | Edit |
| 336 | </Button> |
| 337 | <DropdownMenu> |
| 338 | <DropdownMenuTrigger asChild> |
| 339 | <Button type="default" icon={<EllipsisVertical />} className="w-7" /> |
| 340 | </DropdownMenuTrigger> |
| 341 | <DropdownMenuContent align="end" side="bottom" className="w-48"> |
| 342 | <DropdownMenuItem |
| 343 | className="gap-x-2" |
| 344 | onClick={() => setShowRegenerateSecretConfirm(true)} |
| 345 | > |
| 346 | <RotateCw size={14} className="text-foreground-lighter" /> |
| 347 | <span>Regenerate secret</span> |
| 348 | </DropdownMenuItem> |
| 349 | <DropdownMenuItem |
| 350 | className="gap-x-2" |
| 351 | onClick={() => setEndpointIdPendingDelete(selectedEndpoint.id)} |
| 352 | > |
| 353 | <Trash2 size={14} className="text-foreground-lighter" /> |
| 354 | <span>Delete endpoint</span> |
| 355 | </DropdownMenuItem> |
| 356 | </DropdownMenuContent> |
| 357 | </DropdownMenu> |
| 358 | </> |
| 359 | ) : undefined |
| 360 | } |
| 361 | webhooksHref={webhooksHref} |
| 362 | scopeLabel={scopeLabel} |
| 363 | /> |
| 364 | |
| 365 | <PageContainer size="default"> |
| 366 | <PageSection> |
| 367 | <PageSectionContent> |
| 368 | {!selectedEndpoint ? ( |
| 369 | <PlatformWebhooksEndpointList |
| 370 | filteredEndpoints={filteredEndpoints} |
| 371 | search={search} |
| 372 | webhooksHref={webhooksHref} |
| 373 | onCreateEndpoint={() => setPanel('create')} |
| 374 | onDeleteEndpoint={(id) => setEndpointIdPendingDelete(id)} |
| 375 | onSearchChange={setSearch} |
| 376 | onViewEndpoint={(id) => { |
| 377 | router.push(`${webhooksHref}/${encodeURIComponent(id)}`) |
| 378 | setPanel(null) |
| 379 | }} |
| 380 | /> |
| 381 | ) : ( |
| 382 | <PlatformWebhooksEndpointDetails |
| 383 | deliverySearch={deliverySearch} |
| 384 | filteredDeliveries={filteredDeliveries} |
| 385 | selectedEndpoint={selectedEndpoint} |
| 386 | onDeliverySearchChange={setDeliverySearch} |
| 387 | onOpenDelivery={(id) => { |
| 388 | setDeliveryDetailsTab('event') |
| 389 | setDeliveryId(id) |
| 390 | }} |
| 391 | onRetryDelivery={handleRetryDelivery} |
| 392 | /> |
| 393 | )} |
| 394 | </PageSectionContent> |
| 395 | </PageSection> |
| 396 | </PageContainer> |
| 397 | |
| 398 | <PlatformWebhooksDeliveryDetailsSheet |
| 399 | deliveryAttempt={deliveryAttempt} |
| 400 | deliveryDetailsTab={deliveryDetailsTab} |
| 401 | deliveryEventPayload={deliveryEventPayload} |
| 402 | deliveryResponsePayload={deliveryResponsePayload} |
| 403 | open={!!selectedDelivery} |
| 404 | selectedDelivery={selectedDelivery} |
| 405 | onCopy={handleCopy} |
| 406 | onOpenChange={(open) => !open && setDeliveryId(null)} |
| 407 | onRetryDelivery={handleRetryDelivery} |
| 408 | onTabChange={setDeliveryDetailsTab} |
| 409 | /> |
| 410 | |
| 411 | <PlatformWebhooksEndpointSheet |
| 412 | visible={isEndpointSheetOpen} |
| 413 | mode={panel === 'create' ? 'create' : 'edit'} |
| 414 | scope={scope} |
| 415 | orgSlug={scope === 'project' ? selectedOrganization?.slug : undefined} |
| 416 | endpoint={panel === 'edit' ? (selectedEndpoint ?? undefined) : undefined} |
| 417 | enabledOverride={panel === 'edit' ? editEnabledOverride : null} |
| 418 | eventTypes={eventTypeOptions} |
| 419 | onClose={() => { |
| 420 | setPanel(null) |
| 421 | setEditEnabledOverride(null) |
| 422 | }} |
| 423 | onSubmit={handleUpsertEndpoint} |
| 424 | /> |
| 425 | |
| 426 | <AlertDialog |
| 427 | open={!!endpointPendingDelete} |
| 428 | onOpenChange={(open) => !open && setEndpointIdPendingDelete(null)} |
| 429 | > |
| 430 | <AlertDialogContent> |
| 431 | <AlertDialogHeader> |
| 432 | <AlertDialogTitle>Delete endpoint</AlertDialogTitle> |
| 433 | <AlertDialogDescription>{deleteEndpointDescription}</AlertDialogDescription> |
| 434 | </AlertDialogHeader> |
| 435 | {endpointPendingDelete && ( |
| 436 | <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"> |
| 437 | {endpointPendingDelete.url} |
| 438 | </pre> |
| 439 | )} |
| 440 | <AlertDialogFooter> |
| 441 | <AlertDialogCancel>Cancel</AlertDialogCancel> |
| 442 | <AlertDialogAction variant="danger" onClick={handleDeleteEndpoint}> |
| 443 | Delete endpoint |
| 444 | </AlertDialogAction> |
| 445 | </AlertDialogFooter> |
| 446 | </AlertDialogContent> |
| 447 | </AlertDialog> |
| 448 | |
| 449 | <AlertDialog open={showRegenerateSecretConfirm} onOpenChange={setShowRegenerateSecretConfirm}> |
| 450 | <AlertDialogContent> |
| 451 | <AlertDialogHeader> |
| 452 | <AlertDialogTitle>Regenerate secret</AlertDialogTitle> |
| 453 | <AlertDialogDescription> |
| 454 | This will rotate the current signing secret used for webhook signature verification. |
| 455 | </AlertDialogDescription> |
| 456 | </AlertDialogHeader> |
| 457 | <AlertDialogFooter> |
| 458 | <AlertDialogCancel>Cancel</AlertDialogCancel> |
| 459 | <AlertDialogAction variant="warning" onClick={handleRegenerateSecret}> |
| 460 | Regenerate |
| 461 | </AlertDialogAction> |
| 462 | </AlertDialogFooter> |
| 463 | </AlertDialogContent> |
| 464 | </AlertDialog> |
| 465 | |
| 466 | <AlertDialog |
| 467 | open={!!signingSecretReveal} |
| 468 | onOpenChange={(open) => { |
| 469 | if (open) return |
| 470 | setSigningSecretReveal(null) |
| 471 | clearPendingSigningSecretReveal(scope) |
| 472 | }} |
| 473 | > |
| 474 | <AlertDialogContent> |
| 475 | <AlertDialogHeader> |
| 476 | <AlertDialogTitle>Signing secret</AlertDialogTitle> |
| 477 | <AlertDialogDescription> |
| 478 | Use this secret to verify webhook signatures using the{' '} |
| 479 | <InlineLink href="https://www.standardwebhooks.com/">Standard Webhooks</InlineLink>{' '} |
| 480 | specification. |
| 481 | </AlertDialogDescription> |
| 482 | </AlertDialogHeader> |
| 483 | {/* Content */} |
| 484 | <div className="space-y-4 mx-5 pb-5"> |
| 485 | <div className="space-y-1"> |
| 486 | <Label>Signing secret</Label> |
| 487 | <Input |
| 488 | copy |
| 489 | readOnly |
| 490 | value={signingSecretReveal?.signingSecret ?? ''} |
| 491 | onChange={() => {}} |
| 492 | onCopy={() => toast.success('Copied signing secret')} |
| 493 | /> |
| 494 | </div> |
| 495 | <div> |
| 496 | <Admonition |
| 497 | type="warning" |
| 498 | title="This secret won’t be shown again" |
| 499 | description="Copy and store it securely now. You will not be able to view or copy it again after closing this dialog." |
| 500 | /> |
| 501 | </div> |
| 502 | </div> |
| 503 | <AlertDialogFooter> |
| 504 | <AlertDialogAction |
| 505 | onClick={() => { |
| 506 | setSigningSecretReveal(null) |
| 507 | clearPendingSigningSecretReveal(scope) |
| 508 | }} |
| 509 | > |
| 510 | I’ve stored the secret |
| 511 | </AlertDialogAction> |
| 512 | </AlertDialogFooter> |
| 513 | </AlertDialogContent> |
| 514 | </AlertDialog> |
| 515 | </> |
| 516 | ) |
| 517 | } |