Skip to content

Commit 753ede6

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
fix(webapp): show an empty state on the API keys page
The API keys page now prompts you to create your first key instead of showing an empty table. Mono-RevId: 1a4a62d57e67d8b13900b66d4fbaf6684cbd5435
1 parent 9f8cc32 commit 753ede6

2 files changed

Lines changed: 166 additions & 123 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
The API keys page now prompts you to create your first key instead of showing an empty table.

‎apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx‎

Lines changed: 160 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { CopyButton } from "~/components/primitives/CopyButton";
3333
import { DateTime } from "~/components/primitives/DateTime";
3434
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
3535
import { FormButtons } from "~/components/primitives/FormButtons";
36+
import { Header2 } from "~/components/primitives/Headers";
3637
import { Hint } from "~/components/primitives/Hint";
3738
import { Input } from "~/components/primitives/Input";
3839
import { InputGroup } from "~/components/primitives/InputGroup";
@@ -45,6 +46,7 @@ import SegmentedControl from "~/components/primitives/SegmentedControl";
4546
import { Switch } from "~/components/primitives/Switch";
4647
import {
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

Comments
 (0)