mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? This is a follow up for both our classic and scoped PAT's to add events for tracking created and deleted. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Enhanced telemetry for access token creation and removal. * Creation events now include token type (classic or scoped), expiry setting, scope/resource access and permission counts when applicable. * Removal events now include token type (classic or scoped). * Instrumentation added across token creation and deletion flows to improve analytics and auditing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
164 lines
5.3 KiB
TypeScript
164 lines
5.3 KiB
TypeScript
import { 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,
|
|
} from 'ui'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table'
|
|
|
|
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 AlertError from '@/components/ui/AlertError'
|
|
import { useAccessTokenDeleteMutation } from '@/data/access-tokens/access-tokens-delete-mutation'
|
|
import { AccessToken, useAccessTokensQuery } from '@/data/access-tokens/access-tokens-query'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
export interface AccessTokenListProps {
|
|
searchString?: string
|
|
onDeleteSuccess: (id: number) => void
|
|
}
|
|
|
|
export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTokenListProps) => {
|
|
const track = useTrack()
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [token, setToken] = useState<AccessToken | undefined>(undefined)
|
|
const [sort, setSort] = useQueryState(
|
|
'sort',
|
|
parseAsStringLiteral<AccessTokenSort>(ACCESS_TOKEN_SORT_VALUES).withDefault('created_at:desc')
|
|
)
|
|
|
|
const { data: tokens, error, isPending: isLoading, isError } = useAccessTokensQuery()
|
|
|
|
const { mutate: deleteToken } = useAccessTokenDeleteMutation({
|
|
onSuccess: (_, vars) => {
|
|
track('access_token_removed', { tokenType: 'classic' })
|
|
onDeleteSuccess(vars.id)
|
|
toast.success('Successfully deleted access token')
|
|
setIsOpen(false)
|
|
},
|
|
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
|
|
|
|
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} />
|
|
<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
|
|
type="default"
|
|
title="More options"
|
|
className="w-7"
|
|
icon={<MoreVertical />}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
|
<DropdownMenuItem
|
|
className="gap-x-2"
|
|
onClick={() => {
|
|
setToken(x)
|
|
setIsOpen(true)
|
|
}}
|
|
>
|
|
<Trash size={12} />
|
|
<p>Delete token</p>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableContainer>
|
|
|
|
<ConfirmationModal
|
|
visible={isOpen}
|
|
variant="destructive"
|
|
title="Confirm to delete"
|
|
confirmLabel="Delete"
|
|
confirmLabelLoading="Deleting"
|
|
onCancel={() => setIsOpen(false)}
|
|
onConfirm={() => {
|
|
if (token) deleteToken({ id: token.id })
|
|
}}
|
|
>
|
|
<p className="py-4 text-sm text-foreground-light">
|
|
This action cannot be undone. Are you sure you want to delete "{token?.name}" token?
|
|
</p>
|
|
</ConfirmationModal>
|
|
</>
|
|
)
|
|
}
|