SecretRow.tsx159 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import dayjs from 'dayjs'
4import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react'
5import { parseAsString, useQueryState } from 'nuqs'
6import { useState } from 'react'
7import {
8 Button,
9 DropdownMenu,
10 DropdownMenuContent,
11 DropdownMenuSeparator,
12 DropdownMenuTrigger,
13} from 'ui'
14import { Input } from 'ui-patterns/DataInputs/Input'
15
16import { SecretTableColumn } from './Secrets.types'
17import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
18import { useVaultSecretDecryptedValueQuery } from '@/data/vault/vault-secret-decrypted-value-query'
19import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
20import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
21import type { VaultSecret } from '@/types'
22
23interface SecretRowProps {
24 row: VaultSecret
25 col: SecretTableColumn
26}
27
28export const SecretRow = ({ row, col }: SecretRowProps) => {
29 const { ref } = useParams()
30 const { data: project } = useSelectedProjectQuery()
31 const [revealSecret, setRevealSecret] = useState(false)
32 const name = row?.name ?? 'No name provided'
33
34 const [, setSelectedSecretToEdit] = useQueryState('edit', parseAsString)
35 const [, setSelectedSecretToDelete] = useQueryState('delete', parseAsString)
36
37 const { can: canManageSecrets } = useAsyncCheckPermissions(
38 PermissionAction.TENANT_SQL_ADMIN_WRITE,
39 'tables'
40 )
41
42 const { data: revealedValue, isFetching } = useVaultSecretDecryptedValueQuery(
43 {
44 projectRef: ref!,
45 connectionString: project?.connectionString,
46 id: row.id,
47 },
48 {
49 enabled: !!(ref! && row.id) && revealSecret,
50 }
51 )
52
53 if (col.id === 'actions') {
54 return (
55 <div className="flex items-center justify-end w-full" onClick={(e) => e.stopPropagation()}>
56 <DropdownMenu>
57 <DropdownMenuTrigger asChild>
58 <Button title="Manage Secret" type="text" className="px-1" icon={<MoreVertical />} />
59 </DropdownMenuTrigger>
60 <DropdownMenuContent side="bottom" align="end" className="w-40">
61 <DropdownMenuItemTooltip
62 className="gap-x-2"
63 disabled={!canManageSecrets}
64 onClick={() => setSelectedSecretToEdit(row.id)}
65 tooltip={{
66 content: { side: 'left', text: 'You need additional permissions to edit secrets' },
67 }}
68 >
69 <Edit3 size={12} />
70 <p>Edit</p>
71 </DropdownMenuItemTooltip>
72
73 <DropdownMenuSeparator />
74
75 <DropdownMenuItemTooltip
76 className="gap-x-2"
77 disabled={!canManageSecrets}
78 onClick={() => setSelectedSecretToDelete(row.id)}
79 tooltip={{
80 content: {
81 side: 'left',
82 text: 'You need additional permissions to delete secrets',
83 },
84 }}
85 >
86 <Trash size={12} />
87 <p className="text-foreground-light">Delete</p>
88 </DropdownMenuItemTooltip>
89 </DropdownMenuContent>
90 </DropdownMenu>
91 </div>
92 )
93 }
94
95 if (col.id === 'secret_value') {
96 return (
97 <div className="flex items-center gap-2 w-full" onClick={(e) => e.stopPropagation()}>
98 <Button
99 type="text"
100 className="px-1.5"
101 icon={
102 isFetching && revealedValue === undefined ? (
103 <Loader className="animate-spin" size={16} strokeWidth={1.5} />
104 ) : !revealSecret ? (
105 <Eye size={16} strokeWidth={1.5} />
106 ) : (
107 <EyeOff size={16} strokeWidth={1.5} />
108 )
109 }
110 onClick={() => setRevealSecret(!revealSecret)}
111 />
112 <div className="grow min-w-0">
113 {revealSecret && revealedValue !== undefined ? (
114 <Input copy readOnly size="tiny" className="font-mono" value={revealedValue} />
115 ) : (
116 <p className="text-sm font-mono text-foreground">••••••••••••••••••</p>
117 )}
118 </div>
119 </div>
120 )
121 }
122
123 if (col.id === 'updated_at') {
124 return (
125 <div className="w-full flex items-center justify-start">
126 <p className="text-xs text-foreground-light">
127 {row.updated_at === row.created_at ? 'Added' : 'Updated'} on{' '}
128 {dayjs(row.updated_at).format('MMM D, YYYY')}
129 </p>
130 </div>
131 )
132 }
133
134 if (col.id === 'id') {
135 return (
136 <div className="w-full flex items-center">
137 <Key size={12} strokeWidth={2} className="text-foreground-light mr-2" />
138 <p className="text-foreground-light text-xs font-mono truncate" title={row.id}>
139 {row.id}
140 </p>
141 </div>
142 )
143 }
144
145 return (
146 <div className="w-full flex flex-col justify-center">
147 <p className="text-xs text-foreground truncate select-text" title={name}>
148 {name}
149 </p>
150 {row.description !== undefined && row.description !== '' && (
151 <div>
152 <p className="text-xs text-foreground-lighter w-full truncate select-text">
153 {row.description}
154 </p>
155 </div>
156 )}
157 </div>
158 )
159}