diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx index 237f6b381a0..8365f779f0f 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx @@ -168,7 +168,7 @@ const AddNewSecretForm = () => {
- Add new secrets + Add or replace secrets {fields.map((fieldItem, index) => ( @@ -178,7 +178,7 @@ const AddNewSecretForm = () => { name={`secrets.${index}.name`} render={({ field }) => ( - Key + Name { Add another - + +

+ Insert or update multiple secrets at once by pasting key-value pairs +

+
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx index 235f0284c58..576f19937ff 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx @@ -1,18 +1,28 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Trash } from 'lucide-react' +import { Edit2, MoreVertical, Trash } from 'lucide-react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { ProjectSecret } from 'data/secrets/secrets-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { TableCell, TableRow } from 'ui' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + TableCell, + TableRow, +} from 'ui' import { TimestampInfo } from 'ui-patterns' interface EdgeFunctionSecretProps { secret: ProjectSecret onSelectDelete: () => void + onSelectEdit: () => void } -const EdgeFunctionSecret = ({ secret, onSelectDelete }: EdgeFunctionSecretProps) => { +const EdgeFunctionSecret = ({ secret, onSelectEdit, onSelectDelete }: EdgeFunctionSecretProps) => { const { can: canUpdateSecrets } = useAsyncCheckPermissions(PermissionAction.SECRETS_WRITE, '*') // [Joshen] Following API's validation: // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/v1/projects/ref/secrets/secrets.controller.ts#L106 @@ -45,23 +55,63 @@ const EdgeFunctionSecret = ({ secret, onSelectDelete }: EdgeFunctionSecretProps)
- } - className="px-1" - disabled={!canUpdateSecrets || isReservedSecret} - onClick={() => onSelectDelete()} - tooltip={{ - content: { - side: 'bottom', - text: isReservedSecret - ? 'This is a reserved secret and cannot be deleted' - : !canUpdateSecrets - ? 'You need additional permissions to delete edge function secrets' - : undefined, - }, - }} - /> + + +
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx index 01938c432b6..85bf4695c55 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx @@ -15,11 +15,17 @@ import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import AddNewSecretForm from './AddNewSecretForm' import EdgeFunctionSecret from './EdgeFunctionSecret' +import { EditSecretSheet } from './EditSecretSheet' + +type SelectedProjectSecret = { + secret: ProjectSecret + op: 'delete' | 'edit' +} const EdgeFunctionSecrets = () => { const { ref: projectRef } = useParams() const [searchString, setSearchString] = useState('') - const [selectedSecret, setSelectedSecret] = useState() + const [selectedSecret, setSelectedSecret] = useState() const { can: canReadSecrets, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.SECRETS_READ, @@ -33,7 +39,7 @@ const EdgeFunctionSecrets = () => { const { mutate: deleteSecret, isLoading: isDeleting } = useSecretsDeleteMutation({ onSuccess: () => { - toast.success(`Successfully deleted ${selectedSecret?.name}`) + toast.success(`Successfully deleted ${selectedSecret?.secret.name}`) setSelectedSecret(undefined) }, }) @@ -99,7 +105,8 @@ const EdgeFunctionSecrets = () => { setSelectedSecret(secret)} + onSelectEdit={() => setSelectedSecret({ secret, op: 'edit' })} + onSelectDelete={() => setSelectedSecret({ secret, op: 'delete' })} /> )) ) : secrets.length === 0 && searchString.length > 0 ? ( @@ -131,17 +138,23 @@ const EdgeFunctionSecrets = () => { )} + setSelectedSecret(undefined)} + /> + setSelectedSecret(undefined)} onConfirm={() => { if (selectedSecret !== undefined) { - deleteSecret({ projectRef, secrets: [selectedSecret.name] }) + deleteSecret({ projectRef, secrets: [selectedSecret.secret.name] }) } }} > diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx new file mode 100644 index 00000000000..aec6bc30393 --- /dev/null +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx @@ -0,0 +1,280 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useCallback, useEffect, useState, type ReactNode } from 'react' +import { SubmitHandler, useForm, type UseFormReturn } from 'react-hook-form' +import { toast } from 'sonner' +import z from 'zod' + +import { useParams } from 'common' +import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation' +import { ProjectSecret } from 'data/secrets/secrets-query' +import { Eye, EyeOff, X } from 'lucide-react' +import { useLatest } from 'react-use' +import { + Button, + cn, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input, + Input_Shadcn_, + Separator, + Sheet, + SheetClose, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +const FORM_ID = 'edit-secret-sidepanel' + +const FormSchema = z.object({ + name: z.string().min(1, 'Please provide a name for your secret'), + value: z.string().min(1, 'Please provide a value for your secret'), +}) + +type FormSchemaType = z.infer + +interface EditSecretSheetProps { + secret?: ProjectSecret + visible: boolean + onClose: () => void +} + +export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetProps) { + const secretName = useLatest(secret?.name) + const form = useForm({ + resolver: zodResolver(FormSchema), + }) + useEffect(() => { + if (visible) { + form.reset({ + name: secretName.current ?? '', + value: '', + }) + } + }, [form, secretName, visible]) + const isValid = form.formState.isValid + + const { ref: projectRef } = useParams() + const { mutate: updateSecret, isLoading: isUpdating } = useSecretsCreateMutation({ + onSuccess: (_, variables) => { + toast.success(`Successfully updated secret "${variables.secrets[0].name}"`) + onClose() + }, + }) + const onSubmit: SubmitHandler = async ({ name, value }) => { + updateSecret({ + projectRef, + secrets: [{ name, value }], + }) + } + + const { confirmOnClose, modal: closeConfirmationModal } = useConfirmOnClose({ + checkIsDirty: () => form.formState.isDirty, + onClose, + }) + + return ( + + +
+ + + + + + + + {closeConfirmationModal} + + ) +} + +const Header = (): ReactNode => { + return ( + + + + Close + + Edit secret + + ) +} + +type FormBodyProps = { + form: UseFormReturn + onSubmit: SubmitHandler +} + +const FormBody = ({ form, onSubmit }: FormBodyProps): ReactNode => { + return ( + + + + + + + + + + + + + ) +} + +type NameFieldProps = { + form: UseFormReturn +} + +const NameField = ({ form }: NameFieldProps): ReactNode => { + return ( + ( + + + + + + )} + /> + ) +} + +type SecretFieldProps = { + form: UseFormReturn +} + +const SecretField = ({ form }: SecretFieldProps): ReactNode => { + const [showSecretValue, setShowSecretValue] = useState(false) + + return ( + ( + + + +