AccessTokenList.tsx163 lines · main
1import { MoreVertical, Trash } from 'lucide-react'
2import { parseAsStringLiteral, useQueryState } from 'nuqs'
3import { useMemo, useState } from 'react'
4import { toast } from 'sonner'
5import {
6 Button,
7 DropdownMenu,
8 DropdownMenuContent,
9 DropdownMenuItem,
10 DropdownMenuTrigger,
11} from 'ui'
12import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
13import { TableCell, TableRow } from 'ui/src/components/shadcn/ui/table'
14
15import {
16 ACCESS_TOKEN_SORT_VALUES,
17 AccessTokenSort,
18 AccessTokenSortColumn,
19} from './AccessToken.types'
20import { filterAndSortTokens, handleSortChange } from './AccessToken.utils'
21import { RowLoading } from './AccessTokenTable/RowLoading'
22import { TableContainer } from './AccessTokenTable/TableContainer'
23import { ExpiresCell, LastUsedCell, TokenNameCell } from './AccessTokenTable/TokenCells'
24import AlertError from '@/components/ui/AlertError'
25import { useAccessTokenDeleteMutation } from '@/data/access-tokens/access-tokens-delete-mutation'
26import { AccessToken, useAccessTokensQuery } from '@/data/access-tokens/access-tokens-query'
27import { useTrack } from '@/lib/telemetry/track'
28
29export interface AccessTokenListProps {
30 searchString?: string
31 onDeleteSuccess: (id: number) => void
32}
33
34export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTokenListProps) => {
35 const track = useTrack()
36 const [isOpen, setIsOpen] = useState(false)
37 const [token, setToken] = useState<AccessToken | undefined>(undefined)
38 const [sort, setSort] = useQueryState(
39 'sort',
40 parseAsStringLiteral<AccessTokenSort>(ACCESS_TOKEN_SORT_VALUES).withDefault('created_at:desc')
41 )
42
43 const { data: tokens, error, isPending: isLoading, isError } = useAccessTokensQuery()
44
45 const { mutate: deleteToken } = useAccessTokenDeleteMutation({
46 onSuccess: (_, vars) => {
47 track('access_token_removed', { tokenType: 'classic' })
48 onDeleteSuccess(vars.id)
49 toast.success('Successfully deleted access token')
50 setIsOpen(false)
51 },
52 onError: (error) => {
53 toast.error(`Failed to delete access token: ${error.message}`)
54 },
55 })
56
57 const onSortChange = (column: AccessTokenSortColumn) => {
58 handleSortChange(sort, column, setSort)
59 }
60
61 const filteredTokens = useMemo(
62 () => filterAndSortTokens(tokens, searchString, sort),
63 [tokens, searchString, sort]
64 )
65
66 const empty = filteredTokens?.length === 0 && !isLoading
67
68 if (isError) {
69 return (
70 <TableContainer sort={sort} onSortChange={onSortChange}>
71 <TableRow>
72 <TableCell colSpan={4} className="p-0">
73 <AlertError
74 error={error}
75 subject="Failed to retrieve access tokens"
76 className="rounded-none border-0"
77 />
78 </TableCell>
79 </TableRow>
80 </TableContainer>
81 )
82 }
83
84 if (isLoading) {
85 return (
86 <TableContainer sort={sort} onSortChange={onSortChange}>
87 <RowLoading />
88 <RowLoading />
89 </TableContainer>
90 )
91 }
92
93 if (empty) {
94 return (
95 <TableContainer sort={sort} onSortChange={onSortChange}>
96 <TableRow>
97 <TableCell colSpan={4} className="py-12">
98 <p className="text-sm text-center text-foreground">No access tokens found</p>
99 <p className="text-sm text-center text-foreground-light">
100 You do not have any tokens created yet
101 </p>
102 </TableCell>
103 </TableRow>
104 </TableContainer>
105 )
106 }
107
108 return (
109 <>
110 <TableContainer sort={sort} onSortChange={onSortChange}>
111 {filteredTokens?.map((x) => (
112 <TableRow key={x.token_alias}>
113 <TokenNameCell name={x.name} tokenAlias={x.token_alias} />
114 <LastUsedCell lastUsedAt={x.last_used_at} />
115 <ExpiresCell expiresAt={x.expires_at} />
116 <TableCell>
117 <div className="flex items-center justify-end gap-x-2">
118 <DropdownMenu>
119 <DropdownMenuTrigger asChild>
120 <Button
121 type="default"
122 title="More options"
123 className="w-7"
124 icon={<MoreVertical />}
125 />
126 </DropdownMenuTrigger>
127 <DropdownMenuContent side="bottom" align="end" className="w-40">
128 <DropdownMenuItem
129 className="gap-x-2"
130 onClick={() => {
131 setToken(x)
132 setIsOpen(true)
133 }}
134 >
135 <Trash size={12} />
136 <p>Delete token</p>
137 </DropdownMenuItem>
138 </DropdownMenuContent>
139 </DropdownMenu>
140 </div>
141 </TableCell>
142 </TableRow>
143 ))}
144 </TableContainer>
145
146 <ConfirmationModal
147 visible={isOpen}
148 variant="destructive"
149 title="Confirm to delete"
150 confirmLabel="Delete"
151 confirmLabelLoading="Deleting"
152 onCancel={() => setIsOpen(false)}
153 onConfirm={() => {
154 if (token) deleteToken({ id: token.id })
155 }}
156 >
157 <p className="py-4 text-sm text-foreground-light">
158 This action cannot be undone. Are you sure you want to delete "{token?.name}" token?
159 </p>
160 </ConfirmationModal>
161 </>
162 )
163}