signing-key-row.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import { components } from 'api-types'
  2. import dayjs from 'dayjs'
  3. import { motion } from 'framer-motion'
  4. import {
  5. CircleArrowDown,
  6. CircleArrowUp,
  7. Eye,
  8. Key,
  9. MoreVertical,
  10. ShieldOff,
  11. Timer,
  12. Trash2,
  13. } from 'lucide-react'
  14. import {
  15. Badge,
  16. Button,
  17. cn,
  18. DropdownMenu,
  19. DropdownMenuContent,
  20. DropdownMenuItem,
  21. DropdownMenuTrigger,
  22. TableCell,
  23. TableRow,
  24. } from 'ui'
  25. import { TimestampInfo } from 'ui-patterns'
  26. import { AlgorithmHoverCard } from '../algorithm-hover-card'
  27. import { statusColors, statusLabels } from '../jwt.constants'
  28. import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
  29. import { JWTSigningKey } from '@/data/jwt-signing-keys/jwt-signing-keys-query'
  30. interface SigningKeyRowProps {
  31. signingKey: components['schemas']['SigningKeyResponse']
  32. setSelectedKey: (key?: JWTSigningKey) => void
  33. setShownDialog: (dialog?: 'key-details' | 'revoke' | 'delete') => void
  34. handlePreviouslyUsedKey: (keyId: string) => void
  35. handleStandbyKey: (keyId: string) => void
  36. legacyKey?: JWTSigningKey | null
  37. standbyKey?: JWTSigningKey | null
  38. isLoading?: boolean
  39. }
  40. const MotionTableRow = motion.create(TableRow)
  41. export const SigningKeyRow = ({
  42. signingKey,
  43. setSelectedKey,
  44. setShownDialog,
  45. handlePreviouslyUsedKey,
  46. handleStandbyKey,
  47. legacyKey,
  48. standbyKey,
  49. isLoading = false,
  50. }: SigningKeyRowProps) => (
  51. <MotionTableRow
  52. key={signingKey.id}
  53. layout
  54. initial={{ opacity: 0, height: 0 }}
  55. animate={{
  56. opacity: 1,
  57. height: 'auto',
  58. transition: { duration: 0.2 },
  59. }}
  60. exit={{ opacity: 0, height: 0 }}
  61. className={cn(
  62. signingKey.status !== 'in_use' ? 'border-b border-dashed border-border' : 'border-b'
  63. )}
  64. >
  65. <TableCell className="w-[150px] pr-0 py-2">
  66. <div className="flex -space-x-px items-center">
  67. <Badge
  68. className={cn(
  69. statusColors[signingKey.status],
  70. 'rounded-r-none',
  71. 'gap-2 w-full h-6',
  72. 'uppercase font-mono',
  73. 'border-r-0'
  74. )}
  75. >
  76. {signingKey.status === 'standby' ? (
  77. <Timer className="shrink-0" size={14} />
  78. ) : (
  79. <Key className="shrink-0" size={14} />
  80. )}
  81. <span className="truncate text-xs">{statusLabels[signingKey.status]}</span>
  82. </Badge>
  83. </div>
  84. </TableCell>
  85. <TableCell className="font-mono truncate w-[315px] pl-0 py-2">
  86. <div className="min-w-0 flex">
  87. <Badge
  88. className={cn(
  89. 'bg-default-200/100 border-foreground-muted',
  90. 'rounded-l-none',
  91. 'gap-2 py-2 h-6 min-w-0 overflow-hidden flex items-center flex-1'
  92. )}
  93. >
  94. <span className="truncate flex-1 text-xs" title={signingKey.id}>
  95. {signingKey.id}
  96. </span>
  97. </Badge>
  98. </div>
  99. </TableCell>
  100. <TableCell className="truncate py-2">
  101. <AlgorithmHoverCard
  102. algorithm={signingKey.algorithm}
  103. legacy={signingKey.id === legacyKey?.id}
  104. />
  105. </TableCell>
  106. {signingKey.status === 'previously_used' || signingKey.status === 'revoked' ? (
  107. <TableCell className="max-w-[100px] text-right py-2 text-sm text-foreground-light whitespace-nowrap hidden lg:table-cell">
  108. <TimestampInfo
  109. className="text-sm"
  110. utcTimestamp={signingKey.updated_at}
  111. label={dayjs(signingKey.updated_at).fromNow()}
  112. />
  113. </TableCell>
  114. ) : (
  115. <TableCell />
  116. )}
  117. <TableCell className="text-right py-2">
  118. {(signingKey.status !== 'in_use' || signingKey.algorithm !== 'HS256') && (
  119. <DropdownMenu>
  120. <DropdownMenuTrigger asChild>
  121. <Button
  122. type="text"
  123. className="px-1.5"
  124. loading={isLoading}
  125. icon={<MoreVertical className="size-4" />}
  126. />
  127. </DropdownMenuTrigger>
  128. <DropdownMenuContent align="end" className="w-52">
  129. {signingKey.algorithm !== 'HS256' && (
  130. <DropdownMenuItem
  131. onSelect={() => {
  132. setSelectedKey(signingKey)
  133. setShownDialog('key-details')
  134. }}
  135. >
  136. <Eye className="mr-2 size-4" />
  137. View key details
  138. </DropdownMenuItem>
  139. )}
  140. {signingKey.status === 'standby' && (
  141. <>
  142. <DropdownMenuItem
  143. onSelect={() => handlePreviouslyUsedKey(signingKey.id)}
  144. className="text-destructive"
  145. >
  146. <CircleArrowDown className="mr-2 size-4" />
  147. Move to previously used
  148. </DropdownMenuItem>
  149. </>
  150. )}
  151. {signingKey.status === 'previously_used' && (
  152. <>
  153. <DropdownMenuItemTooltip
  154. disabled={!!standbyKey}
  155. onSelect={() => handleStandbyKey(signingKey.id)}
  156. tooltip={{
  157. content: {
  158. side: 'left',
  159. text: !!standbyKey
  160. ? 'You may only have one standby key at a time'
  161. : undefined,
  162. },
  163. }}
  164. >
  165. <CircleArrowUp className="mr-2 size-4" />
  166. Move to standby key
  167. </DropdownMenuItemTooltip>
  168. <DropdownMenuItem
  169. onSelect={() => {
  170. setSelectedKey(signingKey)
  171. setShownDialog('revoke')
  172. }}
  173. className="text-destructive"
  174. >
  175. <ShieldOff className="mr-2 size-4" />
  176. Revoke key
  177. </DropdownMenuItem>
  178. </>
  179. )}
  180. {signingKey.status === 'revoked' && (
  181. <>
  182. <DropdownMenuItemTooltip
  183. disabled={!!standbyKey}
  184. onSelect={() => handleStandbyKey(signingKey.id)}
  185. tooltip={{
  186. content: {
  187. side: 'left',
  188. text: !!standbyKey
  189. ? 'You may only have one standby key at a time'
  190. : undefined,
  191. },
  192. }}
  193. >
  194. <CircleArrowUp className="mr-2 size-4" />
  195. Move to standby key
  196. </DropdownMenuItemTooltip>
  197. <DropdownMenuItem
  198. onSelect={() => {
  199. setSelectedKey(signingKey)
  200. setShownDialog('delete')
  201. }}
  202. className="text-destructive"
  203. disabled={legacyKey?.id === signingKey.id}
  204. >
  205. <Trash2 className="mr-2 size-4" />
  206. Delete permanently
  207. </DropdownMenuItem>
  208. </>
  209. )}
  210. </DropdownMenuContent>
  211. </DropdownMenu>
  212. )}
  213. </TableCell>
  214. </MotionTableRow>
  215. )