EditSecretModal.tsx221 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { Eye, EyeOff } from 'lucide-react'
3import { parseAsString, useQueryState } from 'nuqs'
4import { useEffect, useState } from 'react'
5import { useForm, type SubmitHandler } from 'react-hook-form'
6import { toast } from 'sonner'
7import {
8 Button,
9 Dialog,
10 DialogContent,
11 DialogFooter,
12 DialogHeader,
13 DialogSection,
14 DialogSectionSeparator,
15 DialogTitle,
16 Form,
17 FormControl,
18 FormField,
19 Input,
20} from 'ui'
21import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
22import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
23import { z } from 'zod'
24
25import { useVaultSecretDecryptedValueQuery } from '@/data/vault/vault-secret-decrypted-value-query'
26import { useVaultSecretUpdateMutation } from '@/data/vault/vault-secret-update-mutation'
27import { useVaultSecretsQuery } from '@/data/vault/vault-secrets-query'
28import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
29import type { VaultSecret } from '@/types'
30
31const SecretSchema = z.object({
32 name: z.string().min(1, 'Please provide a name for your secret'),
33 description: z.string().optional(),
34 secret: z.string().min(1, 'Please enter your secret value'),
35})
36
37const formId = 'edit-vault-secret-form'
38
39export const EditSecretModal = () => {
40 const { data: project } = useSelectedProjectQuery()
41
42 const { data: secrets = [], isSuccess } = useVaultSecretsQuery({
43 projectRef: project?.ref,
44 connectionString: project?.connectionString,
45 })
46 const [secretIdToEdit, setSelectedSecretToEdit] = useQueryState('edit', parseAsString)
47 const secret = secrets.find((secret) => secret.id === secretIdToEdit)
48
49 const [showSecretValue, setShowSecretValue] = useState(false)
50
51 const { data, isPending: isLoadingSecretValue } = useVaultSecretDecryptedValueQuery(
52 {
53 projectRef: project?.ref,
54 id: secret?.id,
55 connectionString: project?.connectionString,
56 },
57 { enabled: !!project?.ref }
58 )
59
60 const values = {
61 name: secret?.name ?? '',
62 description: secret?.description ?? '',
63 secret: secret?.decryptedSecret ?? data ?? '',
64 }
65
66 const form = useForm<z.infer<typeof SecretSchema>>({
67 resolver: zodResolver(SecretSchema as any),
68 defaultValues: values,
69 values,
70 })
71
72 const { mutate: updateSecret, isPending: isSubmitting } = useVaultSecretUpdateMutation()
73
74 const onSubmit: SubmitHandler<z.infer<typeof SecretSchema>> = async (values) => {
75 if (!project) return console.error('Project is required')
76 if (!secret) return console.error('Secret is required')
77
78 const payload: Partial<VaultSecret> = {
79 secret: values.secret,
80 }
81 if (values.name !== secret.name) payload.name = values.name
82 if (values.description !== secret.description) payload.description = values.description
83
84 if (Object.keys(payload).length > 0) {
85 updateSecret(
86 {
87 projectRef: project.ref,
88 connectionString: project?.connectionString,
89 id: secret.id,
90 ...payload,
91 },
92 {
93 onSuccess: () => {
94 toast.success('Successfully updated secret')
95 setSelectedSecretToEdit(null)
96 },
97 onError: (error) => {
98 toast.error(`Failed to update secret: ${error.message}`)
99 },
100 }
101 )
102 }
103 }
104
105 useEffect(() => {
106 if (isSuccess && !!secretIdToEdit && !secret) {
107 toast('Secret not found')
108 setSelectedSecretToEdit(null)
109 }
110 }, [isSuccess, secretIdToEdit, secret, setSelectedSecretToEdit])
111
112 return (
113 <Dialog
114 open={!!secret}
115 onOpenChange={(open) => {
116 if (!open) {
117 form.reset()
118 setSelectedSecretToEdit(null)
119 }
120 }}
121 >
122 <DialogContent>
123 <DialogHeader>
124 <DialogTitle>Edit secret</DialogTitle>
125 </DialogHeader>
126 <DialogSectionSeparator />
127 {isLoadingSecretValue ? (
128 <DialogSection>
129 <GenericSkeletonLoader />
130 </DialogSection>
131 ) : (
132 <>
133 <DialogSection>
134 <Form {...form}>
135 <form
136 id={formId}
137 className="flex flex-col gap-4"
138 autoComplete="off"
139 onSubmit={form.handleSubmit(onSubmit)}
140 >
141 <FormField
142 key="name"
143 name="name"
144 control={form.control}
145 render={({ field }) => (
146 <FormItemLayout name="name" label="Name">
147 <FormControl>
148 <Input id="name" {...field} />
149 </FormControl>
150 </FormItemLayout>
151 )}
152 />
153 <FormField
154 key="description"
155 name="description"
156 control={form.control}
157 render={({ field }) => (
158 <FormItemLayout
159 name="description"
160 label="Description"
161 labelOptional="Optional"
162 >
163 <FormControl>
164 <Input id="description" {...field} data-lpignore="true" />
165 </FormControl>
166 </FormItemLayout>
167 )}
168 />
169 <FormField
170 key="secret"
171 name="secret"
172 control={form.control}
173 render={({ field }) => (
174 <FormItemLayout name="secret" label="Secret value">
175 <FormControl>
176 <div className="relative">
177 <Input
178 id="secret"
179 type={showSecretValue ? 'text' : 'password'}
180 {...field}
181 data-lpignore="true"
182 />
183 <Button
184 type="default"
185 title={showSecretValue ? `Hide secret value` : `Show secret value`}
186 aria-label={
187 showSecretValue ? `Hide secret value` : `Show secret value`
188 }
189 className="absolute right-1 top-1 w-7"
190 icon={showSecretValue ? <EyeOff /> : <Eye />}
191 onClick={() => setShowSecretValue(!showSecretValue)}
192 />
193 </div>
194 </FormControl>
195 </FormItemLayout>
196 )}
197 />
198 </form>
199 </Form>
200 </DialogSection>
201 <DialogFooter>
202 <Button
203 type="default"
204 disabled={isSubmitting}
205 onClick={() => {
206 form.reset()
207 setSelectedSecretToEdit(null)
208 }}
209 >
210 Cancel
211 </Button>
212 <Button form={formId} htmlType="submit" loading={isSubmitting}>
213 Update secret
214 </Button>
215 </DialogFooter>
216 </>
217 )}
218 </DialogContent>
219 </Dialog>
220 )
221}