EditSecretModal.tsx 7.2 KB

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