ScopedTokenList.tsx 6.2 KB

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