mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
Scoped PAT: View token permissions (#48003)
Third stacked PR for scoped PAT extracted from the prototype #47783 to ease reviewing. Previous stacked PR is #47979 Next stacked PR is #47999 This PR add a way to view the tokens permissions <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “View permissions” option for scoped access tokens. * Scoped token permissions now open in a dedicated viewing panel. * Scoped token support is enabled by default. * **Bug Fixes** * Improved token deletion confirmation to clearly identify the selected token. * Deletion dialogs now close correctly after successful removal. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
bccc18af8e
commit
eb4aafa37a
2 files changed
+31
-25
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { MoreVertical, Trash } from 'lucide-react'
|
||||
import { Key, MoreVertical, Trash } from 'lucide-react'
|
||||
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -22,6 +22,7 @@ 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'
|
||||
@@ -39,16 +40,14 @@ export const AccessTokenList = ({
|
||||
onDeleteSuccess,
|
||||
}: AccessTokenListProps) => {
|
||||
const track = useTrack()
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
|
||||
const [token, setToken] = useState<MergedAccessToken | undefined>(undefined)
|
||||
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 { tokens, error, isLoading, isError } = useMergedAccessTokens({ scopedTokensEnabled })
|
||||
|
||||
const { mutate: deleteClassicToken, isPending: isPendingClassicToken } =
|
||||
useAccessTokenDeleteMutation({
|
||||
@@ -56,7 +55,7 @@ export const AccessTokenList = ({
|
||||
track('access_token_removed', { tokenType: 'classic' })
|
||||
onDeleteSuccess(vars.id)
|
||||
toast.success('Successfully deleted access token')
|
||||
setIsDeleteOpen(false)
|
||||
setTokenToDelete(undefined)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to delete access token: ${error.message}`)
|
||||
@@ -69,7 +68,7 @@ export const AccessTokenList = ({
|
||||
track('access_token_removed', { tokenType: 'scoped' })
|
||||
onDeleteSuccess(vars.id)
|
||||
toast.success('Successfully deleted access token')
|
||||
setIsDeleteOpen(false)
|
||||
setTokenToDelete(undefined)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to delete access token: ${error.message}`)
|
||||
@@ -88,9 +87,9 @@ export const AccessTokenList = ({
|
||||
const empty = filteredTokens?.length === 0 && !isLoading
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (!token) return
|
||||
if (token.kind === 'classic') deleteClassicToken({ id: token.id })
|
||||
else deleteScopedToken({ id: token.id })
|
||||
if (!tokenToDelete) return
|
||||
if (tokenToDelete.kind === 'classic') deleteClassicToken({ id: tokenToDelete.id as number })
|
||||
else deleteScopedToken({ id: tokenToDelete.id as string })
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
@@ -158,13 +157,13 @@ export const AccessTokenList = ({
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onClick={() => {
|
||||
setToken(x)
|
||||
setIsDeleteOpen(true)
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
@@ -177,19 +176,26 @@ export const AccessTokenList = ({
|
||||
</TableContainer>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isDeleteOpen}
|
||||
visible={tokenToDelete != null}
|
||||
variant="destructive"
|
||||
title="Confirm to delete"
|
||||
confirmLabel="Delete"
|
||||
confirmLabelLoading="Deleting"
|
||||
onCancel={() => setIsDeleteOpen(false)}
|
||||
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 "{token?.name}" token?
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+4
-4
@@ -30,10 +30,10 @@ interface UseMergedAccessTokensOptions {
|
||||
* the consuming list (via filterAndSortTokens), so this only merges + tags.
|
||||
*/
|
||||
export const useMergedAccessTokens = ({
|
||||
scopedTokensEnabled: scopedEnabled,
|
||||
scopedTokensEnabled,
|
||||
}: UseMergedAccessTokensOptions = {}) => {
|
||||
const classic = useAccessTokensQuery()
|
||||
const scoped = useScopedAccessTokensQuery({ enabled: scopedEnabled })
|
||||
const scoped = useScopedAccessTokensQuery({ enabled: scopedTokensEnabled })
|
||||
|
||||
return useMemo(() => {
|
||||
const classicTokens: MergedAccessToken[] = (classic.data ?? []).map((token) => ({
|
||||
@@ -47,7 +47,7 @@ export const useMergedAccessTokens = ({
|
||||
return {
|
||||
tokens: [...classicTokens, ...scopedTokens],
|
||||
// Classic drives the primary states; a scoped fetch that is still loading shouldn't block the list.
|
||||
isLoading: classic.isPending || (scopedEnabled && scoped.isPending),
|
||||
isLoading: classic.isPending || (scopedTokensEnabled && scoped.isPending),
|
||||
isError: classic.isError,
|
||||
error: classic.error,
|
||||
isSuccess: classic.isSuccess,
|
||||
@@ -60,6 +60,6 @@ export const useMergedAccessTokens = ({
|
||||
classic.isSuccess,
|
||||
scoped.data,
|
||||
scoped.isPending,
|
||||
scopedEnabled,
|
||||
scopedTokensEnabled,
|
||||
])
|
||||
}
|
||||
Reference in new issue
Block a user