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