@@ -33,6 +33,7 @@ import { CopyButton } from "~/components/primitives/CopyButton";
3333import { DateTime } from "~/components/primitives/DateTime" ;
3434import { Dialog , DialogContent , DialogHeader , DialogTrigger } from "~/components/primitives/Dialog" ;
3535import { FormButtons } from "~/components/primitives/FormButtons" ;
36+ import { Header2 } from "~/components/primitives/Headers" ;
3637import { Hint } from "~/components/primitives/Hint" ;
3738import { Input } from "~/components/primitives/Input" ;
3839import { InputGroup } from "~/components/primitives/InputGroup" ;
@@ -45,6 +46,7 @@ import SegmentedControl from "~/components/primitives/SegmentedControl";
4546import { Switch } from "~/components/primitives/Switch" ;
4647import {
4748 Table ,
49+ TableBlankRow ,
4850 TableBody ,
4951 TableCell ,
5052 TableCellMenu ,
@@ -327,6 +329,8 @@ export default function Page() {
327329 . join ( "\n" )
328330 : null ;
329331
332+ const hasApiKeys = rootApiKey !== null || apiKeys . length > 0 ;
333+
330334 return (
331335 < PageContainer >
332336 < NavBar >
@@ -357,133 +361,154 @@ export default function Page() {
357361 < PageBody scrollable = { false } >
358362 { canReadApiKeys ? (
359363 < div className = "max-h-full min-h-full overflow-y-auto border-t border-grid-dimmed" >
360- < div className = "flex h-fit items-center justify-end gap-2 p-2" >
361- < div className = "flex items-center gap-2" >
362- { envBlock ? (
363- < EnvironmentVariablesDialog
364- environmentType = { environment . type }
365- envBlock = { envBlock }
366- />
367- ) : null }
368- < RevokedFilter checked = { showRevoked } />
369- < NewApiKeyDialog
370- canWrite = { canWriteApiKeys }
371- availableTasks = { availableTasks }
372- presets = { presets }
373- isRbacPluginAvailable = { isRbacPluginAvailable }
374- environment = { apiKeyEnvironmentLabel }
375- />
376- </ div >
377- </ div >
378-
379- < Table >
380- < ApiKeyTableHeader />
381- < TableBody >
382- { rootApiKey ? (
383- < TableRow className = "h-[3.25rem] [&_td]:py-2" >
384- < TableCell >
385- < div className = "flex items-center gap-1.5 text-text-bright" >
386- < KeyIcon className = "size-4" />
387- { rootApiKey . name }
388- < Badge variant = "extra-small" > Root</ Badge >
389- </ div >
390- </ TableCell >
391- < TableCell >
392- < div className = "flex w-64 items-center justify-between gap-2" >
393- < span className = "font-mono text-text-dimmed" > { rootApiKey . obfuscated } </ span >
394- < CopyButton value = { rootApiKey . value } variant = "icon" size = "small" />
395- </ div >
396- </ TableCell >
397- < TableCell >
398- < ApiKeyStatus now = { loadedAt } />
399- </ TableCell >
400- < TableCell >
401- < ApiKeyAccess label = "No restrictions" />
402- </ TableCell >
403- < TableCell > –</ TableCell >
404- < TableCell > –</ TableCell >
405- < TableCell > –</ TableCell >
406- < TableCellMenu
407- isSticky
408- className = "w-32"
409- hiddenButtons = {
410- < >
411- { canWriteApiKeys ? (
412- < RegenerateApiKeyModal
413- id = { environment . keyEnvironmentId }
414- title = { environmentFullTitle ( apiKeyEnvironmentLabel ) }
415- hasVercelIntegration = { hasVercelIntegration }
416- isDevelopment = { environment . type === "DEVELOPMENT" }
417- />
418- ) : null }
419- < DisableRootApiKeyButton canWrite = { canWriteApiKeys } />
420- </ >
421- }
364+ { /* The dialog wraps both triggers so it stays mounted when creating the first key swaps
365+ the empty state for the table: the new key is only shown once, in this dialog. */ }
366+ < NewApiKeyDialog
367+ availableTasks = { availableTasks }
368+ presets = { presets }
369+ isRbacPluginAvailable = { isRbacPluginAvailable }
370+ environment = { apiKeyEnvironmentLabel }
371+ >
372+ < div className = "flex h-fit items-center justify-end gap-2 p-2" >
373+ < div className = "flex items-center gap-2" >
374+ { envBlock ? (
375+ < EnvironmentVariablesDialog
376+ environmentType = { environment . type }
377+ envBlock = { envBlock }
422378 />
423- </ TableRow >
424- ) : null }
379+ ) : null }
380+ < RevokedFilter checked = { showRevoked } />
381+ { hasApiKeys ? (
382+ < NewApiKeyButton canWrite = { canWriteApiKeys } variant = "primary/small" />
383+ ) : null }
384+ </ div >
385+ </ div >
425386
426- { apiKeys . map ( ( apiKey ) => {
427- const isExpired = apiKey . expiresAt
428- ? new Date ( apiKey . expiresAt ) . getTime ( ) <= loadedAt
429- : false ;
430- const cannotAuthenticate = Boolean ( apiKey . revokedAt ) || isExpired ;
431- const cannotRevoke = Boolean ( apiKey . revokedAt ) || isExpired ;
432- const creator =
433- apiKey . createdBy ?. displayName ??
434- apiKey . createdBy ?. name ??
435- apiKey . createdBy ?. email ??
436- "–" ;
437-
438- return (
439- < TableRow
440- key = { apiKey . id }
441- disabled = { cannotAuthenticate }
442- className = "h-[3.25rem] [&_td]:py-2"
443- >
444- < TableCell > { apiKey . name } </ TableCell >
387+ < Table >
388+ < ApiKeyTableHeader />
389+ < TableBody >
390+ { rootApiKey ? (
391+ < TableRow className = "h-[3.25rem] [&_td]:py-2" >
445392 < TableCell >
446- < span className = "font-mono text-text-dimmed" > { apiKey . obfuscated } </ span >
393+ < div className = "flex items-center gap-1.5 text-text-bright" >
394+ < KeyIcon className = "size-4" />
395+ { rootApiKey . name }
396+ < Badge variant = "extra-small" > Root</ Badge >
397+ </ div >
447398 </ TableCell >
448399 < TableCell >
449- < ApiKeyStatus
450- revokedAt = { apiKey . revokedAt }
451- expiresAt = { apiKey . expiresAt }
452- now = { loadedAt }
453- />
400+ < div className = "flex w-64 items-center justify-between gap-2" >
401+ < span className = "font-mono text-text-dimmed" >
402+ { rootApiKey . obfuscated }
403+ </ span >
404+ < CopyButton value = { rootApiKey . value } variant = "icon" size = "small" />
405+ </ div >
454406 </ TableCell >
455407 < TableCell >
456- < ApiKeyAccess
457- label = { apiKey . access . label }
458- taskIdentifiers = { apiKey . access . taskIdentifiers }
459- usesTaskSelection = { apiKey . access . usesTaskSelection }
460- />
408+ < ApiKeyStatus now = { loadedAt } />
461409 </ TableCell >
462- < TableCell > { creator } </ TableCell >
463410 < TableCell >
464- < DateTime date = { apiKey . createdAt } />
465- </ TableCell >
466- < TableCell >
467- { apiKey . lastUsedAt ? < DateTime date = { apiKey . lastUsedAt } /> : "Never" }
411+ < ApiKeyAccess label = "No restrictions" />
468412 </ TableCell >
413+ < TableCell > –</ TableCell >
414+ < TableCell > –</ TableCell >
415+ < TableCell > –</ TableCell >
469416 < TableCellMenu
470417 isSticky
471418 className = "w-32"
472419 hiddenButtons = {
473- cannotRevoke ? null : (
474- < RevokeApiKeyButton
475- id = { apiKey . id }
476- name = { apiKey . name }
477- canWrite = { canWriteApiKeys }
478- />
479- )
420+ < >
421+ { canWriteApiKeys ? (
422+ < RegenerateApiKeyModal
423+ id = { environment . keyEnvironmentId }
424+ title = { environmentFullTitle ( apiKeyEnvironmentLabel ) }
425+ hasVercelIntegration = { hasVercelIntegration }
426+ isDevelopment = { environment . type === "DEVELOPMENT" }
427+ />
428+ ) : null }
429+ < DisableRootApiKeyButton canWrite = { canWriteApiKeys } />
430+ </ >
480431 }
481432 />
482433 </ TableRow >
483- ) ;
484- } ) }
485- </ TableBody >
486- </ Table >
434+ ) : null }
435+
436+ { apiKeys . map ( ( apiKey ) => {
437+ const isExpired = apiKey . expiresAt
438+ ? new Date ( apiKey . expiresAt ) . getTime ( ) <= loadedAt
439+ : false ;
440+ const cannotAuthenticate = Boolean ( apiKey . revokedAt ) || isExpired ;
441+ const cannotRevoke = Boolean ( apiKey . revokedAt ) || isExpired ;
442+ const creator =
443+ apiKey . createdBy ?. displayName ??
444+ apiKey . createdBy ?. name ??
445+ apiKey . createdBy ?. email ??
446+ "–" ;
447+
448+ return (
449+ < TableRow
450+ key = { apiKey . id }
451+ disabled = { cannotAuthenticate }
452+ className = "h-[3.25rem] [&_td]:py-2"
453+ >
454+ < TableCell > { apiKey . name } </ TableCell >
455+ < TableCell >
456+ < span className = "font-mono text-text-dimmed" > { apiKey . obfuscated } </ span >
457+ </ TableCell >
458+ < TableCell >
459+ < ApiKeyStatus
460+ revokedAt = { apiKey . revokedAt }
461+ expiresAt = { apiKey . expiresAt }
462+ now = { loadedAt }
463+ />
464+ </ TableCell >
465+ < TableCell >
466+ < ApiKeyAccess
467+ label = { apiKey . access . label }
468+ taskIdentifiers = { apiKey . access . taskIdentifiers }
469+ usesTaskSelection = { apiKey . access . usesTaskSelection }
470+ />
471+ </ TableCell >
472+ < TableCell > { creator } </ TableCell >
473+ < TableCell >
474+ < DateTime date = { apiKey . createdAt } />
475+ </ TableCell >
476+ < TableCell >
477+ { apiKey . lastUsedAt ? < DateTime date = { apiKey . lastUsedAt } /> : "Never" }
478+ </ TableCell >
479+ < TableCellMenu
480+ isSticky
481+ className = "w-32"
482+ hiddenButtons = {
483+ cannotRevoke ? null : (
484+ < RevokeApiKeyButton
485+ id = { apiKey . id }
486+ name = { apiKey . name }
487+ canWrite = { canWriteApiKeys }
488+ />
489+ )
490+ }
491+ />
492+ </ TableRow >
493+ ) ;
494+ } ) }
495+
496+ { hasApiKeys ? null : (
497+ < TableBlankRow colSpan = { 8 } >
498+ < div className = "flex flex-col items-center justify-center gap-y-4 py-8" >
499+ < div className = "flex flex-col items-center gap-y-1" >
500+ < Header2 > You don't have any API keys.</ Header2 >
501+ < Paragraph variant = "small" >
502+ Create one so your backend can authenticate with Trigger.dev.
503+ </ Paragraph >
504+ </ div >
505+ < NewApiKeyButton canWrite = { canWriteApiKeys } variant = "primary/medium" />
506+ </ div >
507+ </ TableBlankRow >
508+ ) }
509+ </ TableBody >
510+ </ Table >
511+ </ NewApiKeyDialog >
487512 </ div >
488513 ) : (
489514 < MainHorizontallyCenteredContainer className = "py-6" >
@@ -579,18 +604,40 @@ function RevokedFilter({ checked }: { checked: boolean }) {
579604 ) ;
580605}
581606
582- function NewApiKeyDialog ( {
607+ function NewApiKeyButton ( {
583608 canWrite,
609+ variant,
610+ } : {
611+ canWrite : boolean ;
612+ variant : "primary/small" | "primary/medium" ;
613+ } ) {
614+ return (
615+ < DialogTrigger asChild >
616+ < Button
617+ variant = { variant }
618+ LeadingIcon = { PlusIcon }
619+ shortcut = { { key : "n" } }
620+ disabled = { ! canWrite }
621+ tooltip = { canWrite ? undefined : "You don't have permission to create API keys" }
622+ >
623+ New API key
624+ </ Button >
625+ </ DialogTrigger >
626+ ) ;
627+ }
628+
629+ function NewApiKeyDialog ( {
584630 availableTasks,
585631 presets,
586632 isRbacPluginAvailable,
587633 environment,
634+ children,
588635} : {
589- canWrite : boolean ;
590636 availableTasks : string [ ] ;
591637 presets : ApiKeyPreset [ ] | null ;
592638 isRbacPluginAvailable : boolean ;
593639 environment : React . ComponentProps < typeof EnvironmentCombo > [ "environment" ] ;
640+ children : React . ReactNode ;
594641} ) {
595642 const fetcher = useTypedFetcher < typeof action > ( ) ;
596643 const actionData = fetcher . data as ApiKeyActionData | undefined ;
@@ -647,17 +694,7 @@ function NewApiKeyDialog({
647694 }
648695 } }
649696 >
650- < DialogTrigger asChild >
651- < Button
652- variant = "primary/small"
653- LeadingIcon = { PlusIcon }
654- shortcut = { { key : "n" } }
655- disabled = { ! canWrite }
656- tooltip = { canWrite ? undefined : "You don't have permission to create API keys" }
657- >
658- New API key
659- </ Button >
660- </ DialogTrigger >
697+ { children }
661698 < DialogContent
662699 className = { cn (
663700 "max-h-[80vh] grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden p-0 pt-2.5" ,
0 commit comments