chore(studio): consistent table rows across pages (#39507)

* consistent table rows across pages

* remove comments

* match copy

* Nit add loading icon when revealing secret key

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Danny WhiteandJoshen Lim authored and GitHub committed 2025-10-15 10:45:17 +11:00
1 parent 507a00e7f4
commit 6617418e1a
7 files changed
+125 -152

No files matched your search

@@ -54,21 +54,23 @@ export const APIKeyRow = ({
</div>
</TableCell>
<TableCell className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger className="px-1 focus-visible:outline-none" asChild>
<Button
type="text"
size="tiny"
icon={
<MoreVertical size="14" className="text-foreground-light hover:text-foreground" />
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-w-40" align="end">
<APIKeyDeleteDialog apiKey={apiKey} lastSeen={lastSeen} />
</DropdownMenuContent>
</DropdownMenu>
<TableCell className="py-2">
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger className="px-1 focus-visible:outline-none" asChild>
<Button
type="text"
size="tiny"
icon={
<MoreVertical size="14" className="text-foreground-light hover:text-foreground" />
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-w-40" align="end">
<APIKeyDeleteDialog apiKey={apiKey} lastSeen={lastSeen} />
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</MotionTableRow>
)
@@ -38,6 +38,7 @@ export function ApiKeyPill({
const {
data,
error,
isLoading,
refetch: refetchApiKey,
} = useAPIKeyIdQuery(
{
@@ -116,9 +117,7 @@ export function ApiKeyPill({
'w-[100px] sm:w-[140px] md:w-[180px] lg:w-[340px] gap-0 font-mono rounded-full',
isSecret ? 'overflow-hidden' : '',
show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0',
'transition-all',
'cursor-text',
'relative'
'transition-all cursor-text relative'
)}
style={{ userSelect: 'all' }}
>
@@ -139,6 +138,7 @@ export function ApiKeyPill({
<Button
type="outline"
className="rounded-full px-2 pointer-events-auto"
loading={show && isLoading}
icon={show ? <EyeOff strokeWidth={2} /> : <Eye strokeWidth={2} />}
onClick={onSubmitToggle}
disabled={isRestricted}
@@ -1,7 +1,11 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
Dialog,
DialogContent,
@@ -12,18 +16,13 @@ import {
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
Alert,
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useAPIKeyCreateMutation } from 'data/api-keys/api-key-create-mutation'
@@ -85,7 +84,7 @@ const CreateSecretAPIKeyDialog = () => {
<Dialog open={visible} onOpenChange={onClose}>
<DialogTrigger asChild>
<Button type="default" className="mt-2" icon={<Plus />}>
Add new secret key
New secret key
</Button>
</DialogTrigger>
<DialogContent>
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import { ReactNode, useMemo, useRef } from 'react'
import { useMemo, useRef } from 'react'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
@@ -8,11 +8,11 @@ import { FormHeader } from 'components/ui/Forms/FormHeader'
import { APIKeysData, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Card, CardContent, EyeOffIcon, Skeleton, cn } from 'ui'
import { Card, EyeOffIcon } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
@@ -75,106 +75,59 @@ export const SecretAPIKeys = () => {
const empty = secretApiKeys?.length === 0 && !isLoadingApiKeys && !isLoadingPermissions
const RowLoading = () => (
<TableRow>
<TableCell>
<Skeleton className="max-w-12 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="max-w-60 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="max-w-60 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="w-2 h-4 rounded-full" />
</TableCell>
</TableRow>
)
const TableContainer = ({ children, className }: { children: ReactNode; className?: string }) => (
return (
<div className="pb-30">
<FormHeader
title="Secret keys"
description="These API keys allow privileged access to your project's APIs. Use in servers, functions, workers or other backend components of your application."
actions={<CreateSecretAPIKeyDialog />}
/>
<Card className={cn('w-full overflow-hidden', !empty && 'bg-surface-100', className)}>
<CardContent className="p-0">
<Table className="p-5 table-auto">
<TableHeader>
<TableRow className={cn('bg-200', empty && 'hidden')}>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Name
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 pr-0">
API Key
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 hidden lg:table-cell">
Last Seen
</TableHead>
<TableHead className="text-right font-mono uppercase text-xs text-foreground-lighter h-auto py-2" />
{isLoadingApiKeys || isLoadingPermissions ? (
<GenericSkeletonLoader />
) : !canReadAPIKeys ? (
<Card>
<div className="!rounded-b-md overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
<EyeOffIcon />
<p className="text-sm text-foreground">
You do not have permission to read API Secret Keys
</p>
<p className="text-foreground-light">
Contact your organization owner/admin to request access.
</p>
</div>
</Card>
) : isErrorApiKeys ? (
<AlertError error={error} subject="Failed to load secret API keys" />
) : empty ? (
<Card>
<div className="!rounded-b-md overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
<p className="text-sm text-foreground">No secret API keys found</p>
<p className="text-sm text-foreground-light">
Your project is not accessible via secret keys—there are no active secret keys
created.
</p>
</div>
</Card>
) : (
<Card className="bg-surface-100">
<Table>
<TableHeader>
<TableRow className="bg-200">
<TableHead>Name</TableHead>
<TableHead>API Key</TableHead>
<TableHead className="hidden lg:table-cell">Last Seen</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody className="">{children}</TableBody>
<TableBody>
{secretApiKeys.map((apiKey) => (
<APIKeyRow key={apiKey.id} apiKey={apiKey} lastSeen={lastSeen[apiKey.hash]} />
))}
</TableBody>
</Table>
</CardContent>
</Card>
</Card>
)}
</div>
)
if (isLoadingApiKeys || isLoadingPermissions) {
return (
<TableContainer>
<RowLoading />
<RowLoading />
</TableContainer>
)
}
if (!canReadAPIKeys) {
return (
<TableContainer>
<div className="!rounded-b-md overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
<EyeOffIcon />
<p className="text-sm text-foreground">
You do not have permission to read API Secret Keys
</p>
<p className="text-foreground-light">
Contact your organization owner/admin to request access.
</p>
</div>
</TableContainer>
)
}
if (isErrorApiKeys) {
return (
<TableContainer className="border-0">
<AlertError error={error} subject="Failed to load secret API keys" />
</TableContainer>
)
}
if (empty) {
return (
<TableContainer>
<div className="!rounded-b-md overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
<p className="text-sm text-foreground">No secret API keys found</p>
<p className="text-sm text-foreground-light">
Your project is not accessible via secret keys—there are no active secret keys created.
</p>
</div>
</TableContainer>
)
}
return (
<TableContainer>
{secretApiKeys.map((apiKey) => (
<APIKeyRow key={apiKey.id} apiKey={apiKey} lastSeen={lastSeen[apiKey.hash]} />
))}
</TableContainer>
)
}
@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Plus } from 'lucide-react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
@@ -86,7 +87,12 @@ export const CreateCredentialModal = ({ visible, onOpenChange }: CreateCredentia
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button type="default" disabled={disableCreation} className="pointer-events-auto">
<Button
type="default"
icon={<Plus size={14} />}
disabled={disableCreation}
className="pointer-events-auto"
>
New access key
</Button>
</DialogTrigger>
@@ -14,7 +14,6 @@ import {
ScaffoldSectionDescription,
ScaffoldSectionTitle,
} from 'components/layouts/Scaffold'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
@@ -36,6 +35,12 @@ import {
FormField_Shadcn_,
Form_Shadcn_,
Switch,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
WarningIcon,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
@@ -116,7 +121,7 @@ export const S3Connection = () => {
return (
<>
<ScaffoldSection isFullWidth>
<div className="flex items-center justify-between mb-6">
<div className="flex items-end justify-between mb-6">
<div>
<ScaffoldSectionTitle>Connection</ScaffoldSectionTitle>
<ScaffoldSectionDescription>
@@ -225,7 +230,7 @@ export const S3Connection = () => {
</ScaffoldSection>
<ScaffoldSection isFullWidth>
<div className="flex items-center justify-between mb-6">
<div className="flex items-end justify-between mb-6">
<div>
<ScaffoldSectionTitle>Access keys</ScaffoldSectionTitle>
<ScaffoldSectionDescription>
@@ -255,20 +260,26 @@ export const S3Connection = () => {
) : (
<>
{isLoadingStorageCreds ? (
<div className="p-4">
<GenericSkeletonLoader />
</div>
<GenericSkeletonLoader />
) : (
<div className="overflow-x-auto">
<Table
head={[
<Table.th key="description">Description</Table.th>,
<Table.th key="access-key-id">Access key ID</Table.th>,
<Table.th key="created-at">Created at</Table.th>,
<Table.th key="actions" />,
]}
body={
hasStorageCreds ? (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="description" className="min-w-40 lg:min-w-64">
Description
</TableHead>
<TableHead key="access-key-id" className="min-w-64 w-full">
Access key ID
</TableHead>
<TableHead key="created-at" className="min-w-48">
Created at
</TableHead>
<TableHead key="actions" />
</TableRow>
</TableHeader>
<TableBody>
{hasStorageCreds ? (
storageCreds.data?.map((cred) => (
<StorageCredItem
key={cred.id}
@@ -283,18 +294,18 @@ export const S3Connection = () => {
/>
))
) : (
<Table.tr>
<Table.td colSpan={4} className="!rounded-b-md overflow-hidden">
<TableRow>
<TableCell colSpan={4} className="!rounded-b-md overflow-hidden">
<p className="text-sm text-foreground">No access keys created</p>
<p className="text-sm text-foreground-light">
There are no access keys associated with your project yet
</p>
</Table.td>
</Table.tr>
)
}
/>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
)}
</>
)}
@@ -10,6 +10,8 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
TableCell,
TableRow,
} from 'ui'
export const StorageCredItem = ({
@@ -45,20 +47,20 @@ export const StorageCredItem = ({
}
return (
<tr className="h-8 text-ellipsis group">
<td>
<TableRow className="h-8 text-ellipsis group">
<TableCell>
<span className="text-foreground">{description}</span>
</td>
<td>
</TableCell>
<TableCell>
<div className="flex items-center justify-between">
<span className="text-ellipsis font-mono cursor-default">{access_key}</span>
<span className="w-24 text-right opacity-0 group-hover:opacity-100 transition-opacity">
<CopyButton text={access_key} type="default" />
</span>
</div>
</td>
<td>{daysSince(created_at)}</td>
<td className="text-right">
</TableCell>
<TableCell className="text-foreground-lighter">{daysSince(created_at)}</TableCell>
<TableCell className="text-right">
{canRemoveAccessKey && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -82,7 +84,7 @@ export const StorageCredItem = ({
</DropdownMenuContent>
</DropdownMenu>
)}
</td>
</tr>
</TableCell>
</TableRow>
)
}