From b9e08ab8fb1c72a074fa63ec8bfe3b9c700f5e31 Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Wed, 27 May 2026 13:35:41 -0700 Subject: [PATCH] Refine API key dialogs and table in account settings. Co-authored-by: Cursor --- .../supporting/api-key-dialogs.tsx | 32 +++++++++++++------ .../supporting/api-key-table.tsx | 21 +++++++++--- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-dialogs.tsx b/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-dialogs.tsx index 449ec7dea..4056b2742 100644 --- a/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-dialogs.tsx +++ b/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-dialogs.tsx @@ -2,9 +2,9 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { yupObject, yupString } from '@stackframe/stack-shared/dist/schema-fields'; -import { captureError } from '@stackframe/stack-shared/dist/utils/errors'; -import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; -import { ActionDialog, Button, CopyField, Input, Label, Typography } from '@stackframe/stack-ui'; +import { throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { runAsynchronouslyWithAlert } from '@stackframe/stack-shared/dist/utils/promises'; +import { ActionDialog, Alert, AlertDescription, CopyField, Input, Label } from '@stackframe/stack-ui'; import { useState } from "react"; import { useForm } from 'react-hook-form'; import * as yup from "yup"; @@ -21,6 +21,7 @@ export const expiresInOptions = { [1000 * 60 * 60 * 24 * 365]: "1 year", [neverInMs]: "Never", } as const; +const expiresInOptionValues = Object.keys(expiresInOptions); /** * Dialog for creating a new API key @@ -34,10 +35,11 @@ export function CreateApiKeyDialog(props: }) { const user = useUser({ or: props.mockMode ? 'return-null' : 'redirect' }); const [loading, setLoading] = useState(false); + const [submitError, setSubmitError] = useState(null); const apiKeySchema = yupObject({ description: yupString().defined().nonEmpty('Description is required'), - expiresIn: yupString().defined(), + expiresIn: yupString().oneOf(expiresInOptionValues, 'Select a valid expiration').defined(), }); const { register, handleSubmit, formState: { errors }, reset } = useForm({ @@ -50,8 +52,12 @@ export function CreateApiKeyDialog(props: const onSubmit = async (data: yup.InferType) => { setLoading(true); + setSubmitError(null); try { - const expirationMs = parseInt(data.expiresIn); + const expirationMs = Number.parseInt(data.expiresIn, 10); + if (Number.isNaN(expirationMs)) { + throwErr("API key expiration must be one of the predefined expiration options"); + } const expiresAt = expirationMs === neverInMs ? undefined : new Date(Date.now() + expirationMs); const key = await props.createApiKey({ description: data.description, @@ -60,8 +66,9 @@ export function CreateApiKeyDialog(props: props.onOpenChange(false); reset(); props.onKeyCreated?.(key); - } catch (e) { - captureError("api-key-create", { error: e }); + } catch (error) { + setSubmitError("Could not create the API key. Please try again."); + throw error; } finally { setLoading(false); } @@ -77,19 +84,25 @@ export function CreateApiKeyDialog(props: label: "Create", props: { loading, disabled: !user }, onClick: async () => { - await handleSubmit(onSubmit)(); + runAsynchronouslyWithAlert(handleSubmit(onSubmit)); + return "prevent-close"; } }} >
{ e.preventDefault(); - runAsynchronously(handleSubmit(onSubmit)); + runAsynchronouslyWithAlert(handleSubmit(onSubmit)); }}>
{errors.description && {errors.description.message}}
+ {submitError && ( + + {submitError} + + )}
@@ -104,6 +117,7 @@ export function CreateApiKeyDialog(props: ))} + {errors.expiresIn && {errors.expiresIn.message}}
diff --git a/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-table.tsx b/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-table.tsx index e5fa1181e..400732a32 100644 --- a/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-table.tsx +++ b/apps/dashboard/src/components/dashboard-account-settings/supporting/api-key-table.tsx @@ -1,5 +1,6 @@ 'use client'; import { ActionCell, ActionDialog, BadgeCell, DataTable, DataTableColumnHeader, DataTableFacetedFilter, DateCell, SearchToolbarItem, TextCell, standardFilterFn } from "@stackframe/stack-ui"; +import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { ColumnDef, Row, Table } from "@tanstack/react-table"; import { useMemo, useState } from "react"; import { ApiKey } from "./types"; @@ -8,6 +9,16 @@ type ExtendedApiKey = ApiKey & { status: 'valid' | 'expired' | 'revoked', }; +const apiKeyStatusPriority = new Map([ + ["valid", 0], + ["expired", 1], + ["revoked", 2], +]); + +function getApiKeyStatusPriority(status: ExtendedApiKey["status"]) { + return apiKeyStatusPriority.get(status) ?? throwErr(`Missing sort priority for API key status ${status}`); +} + function toolbarRender(table: Table) { return ( <> @@ -100,19 +111,19 @@ const columns: ColumnDef[] = [ export function ApiKeyTable(props: { apiKeys: ApiKey[] }) { const extendedApiKeys = useMemo(() => { const keys = props.apiKeys.map((apiKey) => { - const map = { 'valid': 'valid', 'manually-revoked': 'revoked', 'expired': 'expired' } as const; - const why = apiKey.whyInvalid() || 'valid'; + const why = apiKey.whyInvalid(); + const status = why === null ? "valid" : why === "manually-revoked" ? "revoked" : why; return { ...apiKey, - status: map[why as keyof typeof map], + status, } satisfies ExtendedApiKey; }); // first sort based on status, then by createdAt return keys.sort((a, b) => { if (a.status === b.status) { - return a.createdAt < b.createdAt ? 1 : -1; + return a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0; } - return a.status === 'valid' ? -1 : 1; + return getApiKeyStatusPriority(a.status) - getApiKeyStatusPriority(b.status); }); }, [props.apiKeys]);