signing-key-row.tsx219 lines · main
1import { components } from 'api-types'
2import dayjs from 'dayjs'
3import { motion } from 'framer-motion'
4import {
5 CircleArrowDown,
6 CircleArrowUp,
7 Eye,
8 Key,
9 MoreVertical,
10 ShieldOff,
11 Timer,
12 Trash2,
13} from 'lucide-react'
14import {
15 Badge,
16 Button,
17 cn,
18 DropdownMenu,
19 DropdownMenuContent,
20 DropdownMenuItem,
21 DropdownMenuTrigger,
22 TableCell,
23 TableRow,
24} from 'ui'
25import { TimestampInfo } from 'ui-patterns'
26
27import { AlgorithmHoverCard } from '../algorithm-hover-card'
28import { statusColors, statusLabels } from '../jwt.constants'
29import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
30import { JWTSigningKey } from '@/data/jwt-signing-keys/jwt-signing-keys-query'
31
32interface 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
43const MotionTableRow = motion.create(TableRow)
44
45export 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)