mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
## What kind of change does this PR introduce? Mechanical cleanup on top of the Button default-variant change (#50160). ## What is the current behavior? Many callsites still pass `variant="default"` even though that is now the component default. ## What is the new behavior? Removes redundant static `variant="default"` from legacy `Button` and `ButtonTooltip` callsites. Keeps explicit defaults where they document the API: - `button-default.tsx` and `button-sizes.tsx` demos - `DocsButton`, which pins neutral styling at the wrapper boundary ## To test Studio: - [Auth → Rate Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits): dirty the form so Cancel appears; Cancel stays neutral, Save stays green - [Project Settings → API Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys): `DocsButton` in the header actions stays neutral Design system: - [Design system → Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button): `button-default` / `button-sizes` still show explicit default styling; Primary (green) is restricted to the Primary section (and `asChild`) WWW: - [www → Brand assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets): Download logo kit / Download button kit stay neutral
198 lines
6.7 KiB
TypeScript
198 lines
6.7 KiB
TypeScript
import { Key, MoreVertical, Trash } from 'lucide-react'
|
|
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
|
import { useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
TableCell,
|
|
TableRow,
|
|
} from 'ui'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
|
|
import {
|
|
ACCESS_TOKEN_SORT_VALUES,
|
|
AccessTokenSort,
|
|
AccessTokenSortColumn,
|
|
} from './AccessToken.types'
|
|
import { filterAndSortTokens, handleSortChange } from './AccessToken.utils'
|
|
import { RowLoading } from './AccessTokenTable/RowLoading'
|
|
import { TableContainer } from './AccessTokenTable/TableContainer'
|
|
import { ExpiresCell, LastUsedCell, TokenNameCell } from './AccessTokenTable/TokenCells'
|
|
import { useMergedAccessTokens, type MergedAccessToken } from './hooks/useMergedAccessTokens'
|
|
import { ViewTokenSheet } from './Scoped/ViewTokenSheet'
|
|
import { AlertError } from '@/components/ui/AlertError'
|
|
import { useAccessTokenDeleteMutation } from '@/data/access-tokens/access-tokens-delete-mutation'
|
|
import { useScopedAccessTokenDeleteMutation } from '@/data/scoped-access-tokens/scoped-access-tokens-delete-mutation'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
export interface AccessTokenListProps {
|
|
searchString?: string
|
|
scopedTokensEnabled?: boolean
|
|
onDeleteSuccess: (id: number | string) => void
|
|
}
|
|
|
|
export const AccessTokenList = ({
|
|
searchString = '',
|
|
scopedTokensEnabled,
|
|
onDeleteSuccess,
|
|
}: AccessTokenListProps) => {
|
|
const track = useTrack()
|
|
const [tokenToShow, setTokenToShow] = useState<MergedAccessToken | undefined>(undefined)
|
|
const [tokenToDelete, setTokenToDelete] = useState<MergedAccessToken | undefined>(undefined)
|
|
const [sort, setSort] = useQueryState(
|
|
'sort',
|
|
parseAsStringLiteral<AccessTokenSort>(ACCESS_TOKEN_SORT_VALUES).withDefault('created_at:desc')
|
|
)
|
|
|
|
const { tokens, error, isLoading, isError } = useMergedAccessTokens({ scopedTokensEnabled })
|
|
|
|
const { mutate: deleteClassicToken, isPending: isPendingClassicToken } =
|
|
useAccessTokenDeleteMutation({
|
|
onSuccess: (_, vars) => {
|
|
track('access_token_removed', { tokenType: 'classic' })
|
|
onDeleteSuccess(vars.id)
|
|
toast.success('Successfully deleted access token')
|
|
setTokenToDelete(undefined)
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to delete access token: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const { mutate: deleteScopedToken, isPending: isPendingScopedToken } =
|
|
useScopedAccessTokenDeleteMutation({
|
|
onSuccess: (_, vars) => {
|
|
track('access_token_removed', { tokenType: 'scoped' })
|
|
onDeleteSuccess(vars.id)
|
|
toast.success('Successfully deleted access token')
|
|
setTokenToDelete(undefined)
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to delete access token: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const onSortChange = (column: AccessTokenSortColumn) => {
|
|
handleSortChange(sort, column, setSort)
|
|
}
|
|
|
|
const filteredTokens = useMemo(
|
|
() => filterAndSortTokens(tokens, searchString, sort),
|
|
[tokens, searchString, sort]
|
|
)
|
|
|
|
const empty = filteredTokens?.length === 0 && !isLoading
|
|
|
|
const handleConfirmDelete = () => {
|
|
if (!tokenToDelete) return
|
|
if (tokenToDelete.kind === 'classic') deleteClassicToken({ id: tokenToDelete.id as number })
|
|
else deleteScopedToken({ id: tokenToDelete.id as string })
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<TableContainer sort={sort} onSortChange={onSortChange}>
|
|
<TableRow>
|
|
<TableCell colSpan={4} className="p-0">
|
|
<AlertError
|
|
error={error}
|
|
subject="Failed to retrieve access tokens"
|
|
className="rounded-none border-0"
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableContainer>
|
|
)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<TableContainer sort={sort} onSortChange={onSortChange}>
|
|
<RowLoading />
|
|
<RowLoading />
|
|
</TableContainer>
|
|
)
|
|
}
|
|
|
|
if (empty) {
|
|
return (
|
|
<TableContainer sort={sort} onSortChange={onSortChange}>
|
|
<TableRow>
|
|
<TableCell colSpan={4} className="py-12">
|
|
<p className="text-sm text-center text-foreground">No access tokens found</p>
|
|
<p className="text-sm text-center text-foreground-light">
|
|
You do not have any tokens created yet
|
|
</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableContainer>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<TableContainer sort={sort} onSortChange={onSortChange}>
|
|
{filteredTokens?.map((x) => (
|
|
<TableRow key={x.token_alias}>
|
|
<TokenNameCell
|
|
name={x.name}
|
|
tokenAlias={x.token_alias}
|
|
isClassic={x.kind === 'classic'}
|
|
scopedTokensEnabled={scopedTokensEnabled}
|
|
/>
|
|
<LastUsedCell lastUsedAt={x.last_used_at} />
|
|
<ExpiresCell expiresAt={x.expires_at} />
|
|
<TableCell>
|
|
<div className="flex items-center justify-end gap-x-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button aria-label="More options" className="w-7" icon={<MoreVertical />} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
|
{x.kind === 'scoped' && (
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => setTokenToShow(x)}>
|
|
<Key size={12} />
|
|
<p>View permissions</p>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => setTokenToDelete(x)}>
|
|
<Trash size={12} />
|
|
<p>Delete token</p>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableContainer>
|
|
|
|
<ConfirmationModal
|
|
visible={tokenToDelete != null}
|
|
variant="destructive"
|
|
title="Confirm to delete"
|
|
confirmLabel="Delete"
|
|
confirmLabelLoading="Deleting"
|
|
onCancel={() => setTokenToDelete(undefined)}
|
|
onConfirm={handleConfirmDelete}
|
|
loading={isPendingClassicToken || isPendingScopedToken}
|
|
>
|
|
<p className="py-4 text-sm text-foreground-light">
|
|
This action cannot be undone. Are you sure you want to delete "{tokenToDelete?.name}"
|
|
token?
|
|
</p>
|
|
</ConfirmationModal>
|
|
|
|
<ViewTokenSheet
|
|
visible={tokenToShow != null}
|
|
onClose={() => setTokenToShow(undefined)}
|
|
tokenId={tokenToShow ? String(tokenToShow.id) : undefined}
|
|
/>
|
|
</>
|
|
)
|
|
}
|