From b2cd7a2e34df6bf6ba5f93ae2d895c6ede0da702 Mon Sep 17 00:00:00 2001
From: Drake Costa
Date: Fri, 18 Jul 2025 05:07:21 -0700
Subject: [PATCH] add missing changes
---
.../Vault/Secrets/EditSecretModal.tsx | 290 +++++++++++-------
.../Integrations/Vault/Secrets/SecretRow.tsx | 2 +-
.../__tests__/EditSecretModal.test.tsx | 125 ++++++++
3 files changed, 299 insertions(+), 118 deletions(-)
create mode 100644 apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx
diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx
index 5c19268657a..25f713ac250 100644
--- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx
+++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/EditSecretModal.tsx
@@ -1,31 +1,67 @@
import { isEmpty } from 'lodash'
-import { useEffect, useState } from 'react'
+import { useState, useEffect } from 'react'
+import { type SubmitHandler, useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
import { toast } from 'sonner'
-
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query'
import { useVaultSecretUpdateMutation } from 'data/vault/vault-secret-update-mutation'
import type { VaultSecret } from 'types'
-import { Button, Form, Input, Modal } from 'ui'
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input_Shadcn_,
+} from 'ui'
import { EyeOff, Eye } from 'lucide-react'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
interface EditSecretModalProps {
selectedSecret: VaultSecret | undefined
onClose: () => void
}
+const SecretSchema = z.object({
+ name: z.string().min(1, 'Please provide a name for your secret'),
+ description: z.string().optional(),
+ secret: z.string().min(1, 'Please enter your secret value'),
+})
+
const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => {
const [showSecretValue, setShowSecretValue] = useState(false)
const { project } = useProjectContext()
-
- const { mutateAsync: updateSecret } = useVaultSecretUpdateMutation()
-
- let INITIAL_VALUES = {
- name: selectedSecret?.name ?? '',
- description: selectedSecret?.description ?? '',
- secret: selectedSecret?.decryptedSecret ?? '',
- }
+ const formId = 'edit-vault-secret-form'
+ const { data, isLoading: isLoadingSecretValue } = useVaultSecretDecryptedValueQuery(
+ {
+ projectRef: project?.ref!,
+ id: selectedSecret?.id!,
+ connectionString: project?.connectionString,
+ },
+ { enabled: selectedSecret !== undefined && !!(project?.ref && selectedSecret?.id) }
+ )
+ const form = useForm>({
+ resolver: zodResolver(SecretSchema),
+ defaultValues: {
+ name: selectedSecret?.name ?? '',
+ description: selectedSecret?.description ?? '',
+ secret: selectedSecret?.decryptedSecret ?? data ?? '',
+ },
+ values: {
+ name: selectedSecret?.name ?? '',
+ description: selectedSecret?.description ?? '',
+ secret: selectedSecret?.decryptedSecret ?? data ?? '',
+ },
+ })
useEffect(() => {
if (selectedSecret !== undefined) {
@@ -33,126 +69,146 @@ const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => {
}
}, [selectedSecret])
- const validate = (values: any) => {
- const errors: any = {}
- if (values.name.length === 0) errors.name = 'Please provide a name for your secret'
- if (values.secret.length === 0) errors.secret = 'Please enter your secret value'
- return errors
- }
+ const { mutateAsync: updateSecret, isLoading: isSubmitting } = useVaultSecretUpdateMutation()
- const onUpdateSecret = async (values: any, { setSubmitting }: any) => {
+ const onSubmit: SubmitHandler> = async (values) => {
if (!project) return console.error('Project is required')
- try {
- const payload: Partial = {}
- if (values.name !== selectedSecret?.name) payload.name = values.name
- if (values.description !== selectedSecret?.description)
- payload.description = values.description
- payload.secret = values.secret
+ const payload: Partial = {
+ secret: values.secret,
+ }
+ if (values.name !== selectedSecret?.name) payload.name = values.name
+ if (values.description !== selectedSecret?.description) payload.description = values.description
- if (!isEmpty(payload) && selectedSecret) {
- setSubmitting(true)
- const res = await updateSecret({
+ if (!isEmpty(payload) && selectedSecret) {
+ updateSecret(
+ {
projectRef: project.ref,
connectionString: project?.connectionString,
id: selectedSecret.id,
...payload,
- })
- if (!res.error) {
- toast.success('Successfully updated secret')
- setSubmitting(false)
- onClose()
- } else {
- toast.error(`Failed to update secret: ${res.error.message}`)
- setSubmitting(false)
+ },
+ {
+ onSuccess: () => {
+ toast.success('Successfully updated secret')
+ onClose()
+ },
+ onError: (error) => {
+ toast.error(`Failed to update secret: ${error.message}`)
+ },
}
- }
- } finally {
+ )
}
}
return (
- Edit secret}
+
+
+
+ Edit secret
+
+
+ {isLoadingSecretValue ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
)
}
diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx
index f62dac4d194..0a79deb18af 100644
--- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx
+++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx
@@ -97,7 +97,7 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
- } />
+ } />
diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx
new file mode 100644
index 00000000000..324b9e3ce76
--- /dev/null
+++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx
@@ -0,0 +1,125 @@
+import { describe, expect, it, beforeEach, vi } from 'vitest'
+import { mockAnimationsApi } from 'jsdom-testing-mocks'
+import { screen, waitFor, fireEvent } from '@testing-library/dom'
+import userEvent from '@testing-library/user-event'
+import { render } from 'tests/helpers'
+import EditSecretModal from '../EditSecretModal'
+import { addAPIMock } from 'tests/lib/msw'
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from 'ui'
+import { Edit3, MoreVertical } from 'lucide-react'
+import { useState } from 'react'
+import { VaultSecret } from 'types/ui'
+import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext'
+
+const secret = {
+ id: '47ca58b4-01c5-4a71-8814-c73856b02e0e',
+ name: 'test',
+ description: 'new text',
+ secret: 'NASR0SoksURJ0OorMJ9FzraTzcqSWk5u1PQa2r4c3w9rUVc=',
+ created_at: '2025-07-13 12:57:47.62208+00',
+ updated_at: '2025-07-13 14:51:37.818223+00',
+}
+
+const Page = ({ onClose }: { onClose: () => void }) => {
+ const [selectedSecretToEdit, setSelectedSecretToEdit] = useState()
+ return (
+
+
+
+ } />
+
+
+ setSelectedSecretToEdit(secret)}
+ >
+
+ Edit
+
+
+
+
+ {
+ setSelectedSecretToEdit(undefined)
+ onClose()
+ }}
+ />
+
+ )
+}
+
+mockAnimationsApi()
+
+describe(`EditSecretModal`, () => {
+ beforeEach(() => {
+ // 'http://localhost:3000/api/platform/projects/default'
+ addAPIMock({
+ method: `get`,
+ path: `/platform/projects/:ref`,
+ // @ts-expect-error
+ response: {
+ cloud_provider: 'localhost',
+ id: 1,
+ inserted_at: '2021-08-02T06:40:40.646Z',
+ name: 'Default Project',
+ organization_id: 1,
+ ref: 'default',
+ region: 'local',
+ status: 'ACTIVE_HEALTHY',
+ },
+ })
+ // 'http://localhost:3000/api/platform/pg-meta/default/query?key=projects-default-secrets-47ca58b4-01c5-4a71-8814-c73856b02e0e'
+ // 'http://localhost:3000/api/platform/pg-meta/default/query?key='
+ // useVaultSecretDecryptedValueQuery + useVaultSecretUpdateMutation
+ // both call the same endpoint but execute different SQL queries
+ addAPIMock({
+ method: `post`,
+ path: `/platform/pg-meta/:ref/query`,
+ // @ts-expect-error this path erroneously has a `never` return type when it should be `unknown` since it executes a SQL query
+ response: [{ decrypted_secret: 'bar', update_secret: '' }],
+ })
+ })
+
+ it(`renders a modal pre-filled with the secret's values`, async () => {
+ const onClose = vi.fn()
+ render()
+
+ const menuTrigger = screen.getByRole(`button`, { name: `Manage Secret` })
+ await userEvent.click(menuTrigger)
+ const editOption = await screen.findByRole(`menuitem`, { name: `Edit` })
+ await userEvent.click(editOption)
+
+ await waitFor(() => {
+ expect(screen.getByRole(`dialog`)).toBeInTheDocument()
+ })
+
+ const nameInput = screen.getByLabelText(`Name`)
+ const descriptionInput = screen.getByLabelText(`Description`)
+ const valueInput = screen.getByLabelText(`Secret value`)
+ const togglePasswordButton = screen.getByRole(`button`, { name: `Show secret value` })
+ const submitButton = screen.getByRole(`button`, { name: `Update secret` })
+
+ expect(nameInput).toHaveValue(secret.name)
+ expect(descriptionInput).toHaveValue(secret.description)
+ expect(valueInput).toHaveAttribute(`type`, `password`)
+ await userEvent.click(togglePasswordButton)
+ expect(valueInput).toHaveAttribute(`type`, `text`)
+
+ await userEvent.type(nameInput, `updated-name`)
+ await userEvent.clear(descriptionInput)
+ await userEvent.type(valueInput, `qux`)
+
+ fireEvent.click(submitButton)
+
+ await waitFor(() => expect(onClose).toHaveBeenCalledOnce())
+ })
+})