Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,158 +1,207 @@
import { isEmpty } from 'lodash'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { EyeOff, Eye } from 'lucide-react'
import { type SubmitHandler, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'

import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
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 { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import type { VaultSecret } from 'types'
import { Button, Form, Input, Modal } from 'ui'

interface EditSecretModalProps {
selectedSecret: VaultSecret | undefined
visible: boolean
secret: VaultSecret
onClose: () => void
}

const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => {
const { data: project } = useSelectedProjectQuery()
const [showSecretValue, setShowSecretValue] = useState(false)
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 { mutateAsync: updateSecret } = useVaultSecretUpdateMutation()
const formId = 'edit-vault-secret-form'

let INITIAL_VALUES = {
name: selectedSecret?.name ?? '',
description: selectedSecret?.description ?? '',
secret: selectedSecret?.decryptedSecret ?? '',
const EditSecretModal = ({ visible, secret, onClose }: EditSecretModalProps) => {
const [showSecretValue, setShowSecretValue] = useState(false)
const { data: project } = useSelectedProjectQuery()
const { data, isLoading: isLoadingSecretValue } = useVaultSecretDecryptedValueQuery(
{
projectRef: project?.ref,
id: secret.id,
connectionString: project?.connectionString,
},
{ enabled: !!project?.ref }
)
const values = {
name: secret.name ?? '',
description: secret.description ?? '',
secret: secret.decryptedSecret ?? data ?? '',
}
const form = useForm<z.infer<typeof SecretSchema>>({
resolver: zodResolver(SecretSchema),
defaultValues: values,
values,
})

useEffect(() => {
if (selectedSecret !== undefined) {
setShowSecretValue(false)
}
}, [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 { mutate: 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 !== secret.name) payload.name = values.name
if (values.description !== secret.description) payload.description = values.description

if (!isEmpty(payload) && selectedSecret) {
setSubmitting(true)
const res = await updateSecret({
if (Object.keys(payload).length > 0) {
updateSecret(
{
projectRef: project.ref,
connectionString: project?.connectionString,
id: selectedSecret.id,
id: secret.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={visible}
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>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react'
import type { VaultSecret } from 'types'
import EditSecretModal from './EditSecretModal'

interface SecretRowProps {
secret: VaultSecret
onSelectEdit: (secret: VaultSecret) => void
onSelectRemove: (secret: VaultSecret) => void
}

const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) => {
const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()

const [modal, setModal] = useState<string | null>(null)
const [revealSecret, setRevealSecret] = useState(false)
const name = secret?.name ?? 'No name provided'

Expand All @@ -46,6 +46,8 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
}
)

const onCloseModal = () => setModal(null)

return (
<div className="px-6 py-4 flex items-center space-x-4">
<div className="space-y-1 min-w-[35%] max-w-[35%]">
Expand Down Expand Up @@ -94,17 +96,18 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
{secret.updated_at === secret.created_at ? 'Added' : 'Updated'} on{' '}
{dayjs(secret.updated_at).format('MMM D, YYYY')}
</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>
<TooltipTrigger asChild>
<DropdownMenuItem
className="space-x-2"
disabled={!canManageSecrets}
onClick={() => onSelectEdit(secret)}
onClick={() => setModal(`edit`)}
>
<Edit3 size="14" />
<p>Edit</p>
Expand Down Expand Up @@ -136,6 +139,8 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>

<EditSecretModal visible={modal === `edit`} secret={secret} onClose={onCloseModal} />
</div>
</div>
)
Expand Down
Loading
Loading