From 4b0bd733128440cd9264f445d7e714efa528f08e Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Mon, 14 Jul 2025 19:40:17 +0800 Subject: [PATCH] chore: jwt keys page table layout (#37097) * Improve layout and truncation in SigningKeyRow Added flex and truncation classes to ensure status labels and key IDs are properly truncated and aligned. This enhances the table's appearance and prevents overflow issues for long text. * Improve icon layout and badge styling in UI components Added flex-shrink-0 to icons in AlgorithmHoverCard for better alignment. Updated InfoPill to use min-w-0, overflow-hidden, and improved badge and label layout for consistent appearance and handling of long content. * Remove unused cn import from InfoPill component The cn utility import was removed from InfoPill.tsx as it is no longer used. The Badge component now uses a direct className string instead. * Update JWT key table columns and add rotation info Removed the 'Key ID' column and added a 'Last rotated at' column to the JWT secret keys table. The signing key row now displays the relative time since the key was last updated for previously used keys. * Add Key ID column to JWT secret keys table Introduces a new 'Key ID' column to the JWTSecretKeysTable component for improved visibility and management of JWT secret keys. * Improve JWT key table UI and add tooltip to key ID Updated the JWT secret keys table to enhance the empty state with an icon and explanatory text, and adjusted column alignment and visibility for 'Last rotated at'. Added a tooltip to the key ID for better accessibility and ensured the 'Last rotated at' column displays for both previously used and revoked keys. * Improve API key table layout and responsiveness API key name and description are now grouped together, with description shown under the name. The last seen column is hidden on smaller screens and displays 'Never used' when appropriate. ApiKeyPill max width is now responsive to screen size. The description column was removed from SecretAPIKeys to streamline the table. * Adjust API key pill and input sizing for responsiveness Reduced max-widths for ApiKeyPill and updated PublishableAPIKeys layout to improve responsiveness. ApiKeyInput now uses dynamic min/max widths for better display across breakpoints. * Adjust lg breakpoint min-width for API key input Changed the lg:min-w value from 40rem to 24rem for the API key input field to improve layout responsiveness at large screen sizes. * Update PublishableAPIKeys.tsx * Remove unused file * Minor refactors --------- Co-authored-by: Joshen Lim --- .../interfaces/APIKeys/APIKeyRow.tsx | 21 ++++++++++++---- .../interfaces/APIKeys/APIKeysTable.tsx | 25 ------------------- .../interfaces/APIKeys/ApiKeyPill.tsx | 6 ++--- .../interfaces/APIKeys/PublishableAPIKeys.tsx | 11 +++++--- .../interfaces/APIKeys/SecretAPIKeys.tsx | 6 ++--- .../JwtSecrets/algorithm-hover-card.tsx | 7 +++--- .../jwt-secret-keys-table/index.tsx | 16 ++++++++++-- .../jwt-secret-keys-table/signing-key-row.tsx | 20 ++++++++++----- apps/studio/components/ui/InfoPill.tsx | 23 +++++++++-------- 9 files changed, 71 insertions(+), 64 deletions(-) delete mode 100644 apps/studio/components/interfaces/APIKeys/APIKeysTable.tsx diff --git a/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx b/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx index 2800dcda2d8..1a0eaa02a9e 100644 --- a/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx +++ b/apps/studio/components/interfaces/APIKeys/APIKeyRow.tsx @@ -1,5 +1,6 @@ -import { MoreVertical } from 'lucide-react' +import { APIKeysData } from 'data/api-keys/api-keys-query' import { motion } from 'framer-motion' +import { MoreVertical } from 'lucide-react' import { Button, DropdownMenu, @@ -10,7 +11,6 @@ import { } from 'ui' import { APIKeyDeleteDialog } from './APIKeyDeleteDialog' import { ApiKeyPill } from './ApiKeyPill' -import { APIKeysData } from 'data/api-keys/api-keys-query' export const APIKeyRow = ({ apiKey, @@ -34,14 +34,25 @@ export const APIKeyRow = ({ mass: 1, }} > - {apiKey.name} + +
+ {apiKey.name} +
+ {apiKey.description || No description} +
+
+
- {apiKey.description || '/'} - {lastSeen?.timestamp ?? '/'} + + +
+ {lastSeen?.timestamp ?? Never used} +
+
diff --git a/apps/studio/components/interfaces/APIKeys/APIKeysTable.tsx b/apps/studio/components/interfaces/APIKeys/APIKeysTable.tsx deleted file mode 100644 index 49aa3a94a97..00000000000 --- a/apps/studio/components/interfaces/APIKeys/APIKeysTable.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { AnimatePresence } from 'framer-motion' - -import { APIKeysData } from 'data/api-keys/api-keys-query' -import { APIKeyRow } from './APIKeyRow' - -const APIKeysTable = ({ - apiKeys, -}: { - apiKeys: Extract[] -}) => { - return ( - - - - - {apiKeys.map((apiKey) => ( - - ))} - - -
- ) -} - -export default APIKeysTable diff --git a/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx b/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx index 6024d017720..28d24040b1d 100644 --- a/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx +++ b/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx @@ -1,10 +1,10 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { Eye } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { InputVariants } from '@ui/components/shadcn/ui/input' import { useParams } from 'common' import CopyButton from 'components/ui/CopyButton' @@ -35,7 +35,6 @@ export function ApiKeyPill({ // It fetches the fully revealed API key when needed const { data, - isLoading: isLoadingApiKey, error, refetch: refetchApiKey, } = useAPIKeyIdQuery( @@ -106,7 +105,6 @@ export function ApiKeyPill({ // States for disabling buttons/showing tooltips const isRestricted = isSecret && !canManageSecretKeys - const isLoading = isLoadingPermission return ( <> @@ -118,7 +116,7 @@ export function ApiKeyPill({ isSecret ? 'overflow-hidden' : '', show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0', 'transition-all', - 'max-w-[340px]', + 'max-w-[100px] sm:max-w-[140px] md:max-w-[180px] lg:max-w-[340px]', 'cursor-text', 'relative' )} diff --git a/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx index 99627770315..179df09b662 100644 --- a/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx +++ b/apps/studio/components/interfaces/APIKeys/PublishableAPIKeys.tsx @@ -1,12 +1,14 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useMemo } from 'react' + import { InputVariants } from '@ui/components/shadcn/ui/input' import { useParams } from 'common' import CopyButton from 'components/ui/CopyButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import { useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' -import { useMemo } from 'react' import { cn, EyeOffIcon, Input_Shadcn_, Skeleton, WarningIcon } from 'ui' + // to add in later with follow up PR // import CreatePublishableAPIKeyDialog from './CreatePublishableAPIKeyDialog' // to add in later with follow up PR @@ -38,7 +40,7 @@ export const PublishableAPIKeys = () => { description="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies." />
-
+
Publishable key
@@ -59,7 +61,7 @@ export const PublishableAPIKeys = () => {
) : (
- Publishable key can be safely shared in public + The publishable key can be safely shared publicly
)}
@@ -91,7 +93,8 @@ function ApiKeyInput() { // The default publisahble key will always be the first one const apiKey = publishableApiKeys[0] - const baseClasses = 'flex-1 grow gap-1 rounded-full min-w-[32em]' + const baseClasses = + 'flex-1 grow gap-1 rounded-full min-w-0 max-w-[200px] sm:max-w-[300px] md:max-w-[400px] lg:min-w-[24rem]' const size = 'tiny' if (isApiKeysLoading || isPermissionsLoading) { diff --git a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx index 16a6a60e9fb..5f6c6e1b83f 100644 --- a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx +++ b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx @@ -112,10 +112,8 @@ export const SecretAPIKeys = () => { API Key - - Description - - + + Last Seen diff --git a/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx b/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx index e546c0900e3..1d69c0051a0 100644 --- a/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx @@ -1,5 +1,4 @@ import { LockKeyholeOpen, RectangleEllipsis } from 'lucide-react' -import React from 'react' import { InfoPill } from 'components/ui/InfoPill' import { AlgorithmDetail, algorithmDetails } from './algorithm-details' @@ -9,7 +8,7 @@ interface AlgorithmHoverCardProps { legacy?: boolean } -export const AlgorithmHoverCard: React.FC = ({ algorithm, legacy }) => { +export const AlgorithmHoverCard = ({ algorithm, legacy }: AlgorithmHoverCardProps) => { const details: AlgorithmDetail = algorithmDetails[algorithm] return ( @@ -17,9 +16,9 @@ export const AlgorithmHoverCard: React.FC = ({ algorith label={{legacy ? `Legacy ${details.label}` : details.label}} icon={ algorithm === 'HS256' ? ( - + ) : ( - + ) } title={details.name} diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx index 5b0389385c3..51eb792891c 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx @@ -257,6 +257,9 @@ export default function JWTSecretKeysTable() { Type + + Last rotated at + Actions @@ -280,8 +283,14 @@ export default function JWTSecretKeysTable() { ) : ( -
- No previously used keys +
+ +
+

No previously used keys

+

+ Rotated keys will appear here for verification of existing tokens +

+
)} @@ -312,6 +321,9 @@ export default function JWTSecretKeysTable() { Type + + Last rotated at + Actions diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx index 4459f71e1f2..15ea69469ce 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx @@ -1,3 +1,4 @@ +import dayjs from 'dayjs' import { motion } from 'framer-motion' import { CircleArrowDown, @@ -73,24 +74,26 @@ export const SigningKeyRow = ({ )} > {signingKey.status === 'standby' ? ( - + ) : ( - + )} - {statusLabels[signingKey.status]} + {statusLabels[signingKey.status]}
-
+
- {signingKey.id} + + {signingKey.id} +
@@ -100,6 +103,11 @@ export const SigningKeyRow = ({ legacy={signingKey.id === legacyKey?.id} /> + {(signingKey.status === 'previously_used' || signingKey.status === 'revoked') && ( + + {dayjs(signingKey.updated_at).fromNow()} + + )} {(signingKey.status !== 'in_use' || signingKey.algorithm !== 'HS256') && ( diff --git a/apps/studio/components/ui/InfoPill.tsx b/apps/studio/components/ui/InfoPill.tsx index a1287d2a693..d88beae7ebf 100644 --- a/apps/studio/components/ui/InfoPill.tsx +++ b/apps/studio/components/ui/InfoPill.tsx @@ -1,11 +1,12 @@ -import React from 'react' +import { ExternalLink } from 'lucide-react' +import React, { ReactNode } from 'react' + +import { Badge } from 'ui/src/components/shadcn/ui/badge' import { HoverCard, HoverCardContent, HoverCardTrigger, } from 'ui/src/components/shadcn/ui/hover-card' -import { Badge } from 'ui/src/components/shadcn/ui/badge' -import { ExternalLink } from 'lucide-react' interface Link { url: string @@ -13,19 +14,21 @@ interface Link { } interface InfoPillProps { - label: string | React.ReactNode - icon: React.ReactNode + label: string | ReactNode + icon: ReactNode title: string - description: string | React.ReactNode + description: string | ReactNode links?: Link[] } -export const InfoPill: React.FC = ({ label, icon, title, description, links }) => { +export const InfoPill = ({ label, icon, title, description, links }: InfoPillProps) => { return ( - - - {icon} {label} + + + + {icon} {label} +