mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user