AccessTokenList.tsx 5.3 KB

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