add missing changes

This commit is contained in:
Drake Costa committed 2025-07-18 05:07:21 -07:00
1 parent 4f9092fa23
commit b2cd7a2e34
3 files changed
+299 -118

No files matched your search

@@ -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<z.infer<typeof SecretSchema>>({
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<z.infer<typeof SecretSchema>> = async (values) => {
if (!project) return console.error('Project is required')
try {
const payload: Partial<VaultSecret> = {}
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<VaultSecret> = {
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 (
<Modal
hideFooter
size="medium"
visible={selectedSecret !== undefined}
onCancel={onClose}
header={<h5 className="text-sm text-foreground">Edit secret</h5>}
<Dialog
open={selectedSecret !== undefined}
onOpenChange={(open) => {
if (!open) {
form.reset()
onClose()
}
}}
>
<Form
id="add-new-secret-form"
initialValues={INITIAL_VALUES}
validate={validate}
validateOnBlur={false}
onSubmit={onUpdateSecret}
>
{({ isSubmitting, resetForm }: any) => {
const {
data,
isLoading: isLoadingSecretValue,
isSuccess: isSuccessSecretValue,
// [Joshen] JFYI this is breaking rules of hooks, will be fixed once we move to
// using react hook form instead
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useVaultSecretDecryptedValueQuery(
{
projectRef: project?.ref!,
id: selectedSecret?.id!,
connectionString: project?.connectionString,
},
{ enabled: selectedSecret !== undefined && !!(project?.ref && selectedSecret?.id) }
)
// [Joshen] JFYI this is breaking rules of hooks, will be fixed once we move to
// using react hook form instead
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (selectedSecret !== undefined && isSuccessSecretValue) {
resetForm({
values: { ...INITIAL_VALUES, secret: data },
initialValues: { ...INITIAL_VALUES, secret: data },
})
}
}, [selectedSecret, isSuccessSecretValue])
return isLoadingSecretValue ? (
<Modal.Content>
<GenericSkeletonLoader />
</Modal.Content>
) : (
<>
<Modal.Content className="space-y-4">
<Input id="name" label="Name" />
<Input id="description" label="Description" labelOptional="Optional" />
<Input
id="secret"
type={showSecretValue ? 'text' : 'password'}
label="Secret value"
actions={
<div className="mr-1">
<Button
type="default"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
</div>
}
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex items-center justify-end space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button htmlType="submit" disabled={isSubmitting} loading={isSubmitting}>
Update secret
</Button>
</Modal.Content>
</>
)
}}
</Form>
</Modal>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit secret</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
{isLoadingSecretValue ? (
<DialogSection>
<GenericSkeletonLoader />
</DialogSection>
) : (
<>
<DialogSection>
<Form_Shadcn_ {...form}>
<form
id={formId}
className="flex flex-col gap-4"
autoComplete="off"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField_Shadcn_
key="name"
name="name"
control={form.control}
render={({ field }) => (
<FormItemLayout name="name" label="Name">
<FormControl_Shadcn_>
<Input_Shadcn_ id="name" {...field} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="description"
name="description"
control={form.control}
render={({ field }) => (
<FormItemLayout
name="description"
label="Description"
labelOptional="Optional"
>
<FormControl_Shadcn_>
<Input_Shadcn_ id="description" {...field} data-lpignore="true" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="secret"
name="secret"
control={form.control}
render={({ field }) => (
<FormItemLayout name="secret" label="Secret value">
<FormControl_Shadcn_>
<div className="relative">
<Input_Shadcn_
id="secret"
type={showSecretValue ? 'text' : 'password'}
{...field}
data-lpignore="true"
/>
<Button
type="default"
title={showSecretValue ? `Hide secret value` : `Show secret value`}
aria-label={
showSecretValue ? `Hide secret value` : `Show secret value`
}
className="absolute right-2 top-1 px-3 py-2"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
</div>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</form>
</Form_Shadcn_>
</DialogSection>
<DialogFooter>
<Button
type="default"
disabled={isSubmitting}
onClick={() => {
form.reset()
onClose()
}}
>
Cancel
</Button>
<Button form={formId} htmlType="submit" loading={isSubmitting}>
Update secret
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}
@@ -97,7 +97,7 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" className="px-1" icon={<MoreVertical />} />
<Button title="Manage Secret" type="text" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<Tooltip>
@@ -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<VaultSecret>()
return (
<ProjectContextProvider projectRef="default">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button title="Manage Secret" type="text" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<DropdownMenuItem
className="space-x-2"
disabled={false}
onClick={() => setSelectedSecretToEdit(secret)}
>
<Edit3 size="14" />
<p>Edit</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<EditSecretModal
selectedSecret={selectedSecretToEdit}
onClose={() => {
setSelectedSecretToEdit(undefined)
onClose()
}}
/>
</ProjectContextProvider>
)
}
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(<Page onClose={onClose} />)
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())
})
})