From 47dbbddc91c372ddbee02657cc3e8ae26bcbbd7f Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 12 Jun 2026 08:11:36 -0600 Subject: [PATCH] chore: fix secrets editor for functions to be text area/ support newlines (#46754) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Update to support text area for functions ## Summary by CodeRabbit * **New Features** * Secret inputs now accept and preserve multi-line values and auto-resize to fit content. * Secret values can be masked/unmasked via a show/hide toggle with tooltip; masking uses styled concealment. * Per-secret controls refined: clearer row layout, dedicated remove icon, and add/save controls moved to the card footer. * **Tests** * Added tests validating multi-line secret entry and that submitted payloads include embedded newlines. * Updated tests to assert masking/unmasking behavior via visual security styling. --------- Co-authored-by: kemal --- .../AddNewSecretForm.test.tsx | 69 +++++++++ .../EdgeFunctionSecrets/AddNewSecretForm.tsx | 138 +++++++++++------- .../EdgeFunctionSecrets/EditSecretSheet.tsx | 57 +++++--- .../Vault/Secrets/AddNewSecretModal.tsx | 52 ++++++- .../Vault/Secrets/EditSecretModal.tsx | 24 ++- .../__tests__/EditSecretModal.test.tsx | 5 +- 6 files changed, 268 insertions(+), 77 deletions(-) create mode 100644 apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.test.tsx diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.test.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.test.tsx new file mode 100644 index 00000000000..4ab435cb0e0 --- /dev/null +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { HttpResponse } from 'msw' +import { describe, expect, test } from 'vitest' + +import { AddNewSecretForm } from './AddNewSecretForm' +import type { ProjectSecret } from '@/data/secrets/secrets-query' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +const multilineValue = '-----BEGIN CERTIFICATE-----\nline2\nline3\n-----END CERTIFICATE-----' + +describe('AddNewSecretForm', () => { + test('renders the value field as a textarea so multiline pastes are preserved', () => { + addAPIMock({ + method: 'get', + path: '/v1/projects/:ref/secrets', + response: () => HttpResponse.json([]), + }) + + customRender() + + const nameInput = screen.getByPlaceholderText('e.g. CLIENT_KEY') + expect(nameInput.tagName).toBe('INPUT') + + const textareas = screen.getAllByRole('textbox') + const valueTextarea = textareas.find((el) => el.tagName === 'TEXTAREA') + expect(valueTextarea).toBeDefined() + }) + + test('submits a multiline value with newlines intact', async () => { + const requests: Array<{ ref: string | undefined; body: unknown }> = [] + addAPIMock({ + method: 'post', + path: '/v1/projects/:ref/secrets', + response: async ({ request, params }) => { + requests.push({ ref: params.ref as string | undefined, body: await request.json() }) + return HttpResponse.json({}, { status: 201 }) + }, + }) + addAPIMock({ + method: 'get', + path: '/v1/projects/:ref/secrets', + response: () => HttpResponse.json([]), + }) + + customRender() + + const nameInput = screen.getByPlaceholderText('e.g. CLIENT_KEY') + const saveButton = screen.getByRole('button', { name: 'Save' }) + + await userEvent.type(nameInput, 'SSL_CERT') + + const textareas = screen.getAllByRole('textbox') + const valueTextarea = textareas.find((el) => el.tagName === 'TEXTAREA')! + await userEvent.type(valueTextarea, multilineValue) + + fireEvent.click(saveButton) + + await waitFor(() => { + expect(requests).toEqual([ + { + ref: 'default', + body: [{ name: 'SSL_CERT', value: multilineValue }], + }, + ]) + }) + }) +}) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx index 740b5929094..b2aec399578 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/AddNewSecretForm.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' -import { Eye, EyeOff, MinusCircle } from 'lucide-react' +import { Eye, EyeOff, Trash } from 'lucide-react' import { useState } from 'react' import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -11,14 +11,17 @@ import { CardFooter, CardHeader, CardTitle, + cn, + ExpandingTextArea, Form, FormControl, FormField, - FormItem, - FormLabel, - FormMessage, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' import { DuplicateSecretWarningModal } from './DuplicateSecretWarningModal' @@ -206,81 +209,114 @@ export const AddNewSecretForm = () => { {fields.map((fieldItem, index) => ( -
+
0 && + 'border-t border-default pt-4 -mx-(--card-padding-x) px-(--card-padding-x)' + )} + > ( - - Name - - handlePaste(e.nativeEvent)} + +
+ + handlePaste(e.nativeEvent)} + /> + +
+
)} /> ( - - Value + - +
+ + +
- } - /> + + + {isSecretVisible(fieldItem.id) ? 'Hide value' : 'Show value'} + + +
- - + )} /> - -
))} - -

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

- +
+ + +
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx index d0dfc795df0..d39c49861f6 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EditSecretSheet.tsx @@ -17,8 +17,8 @@ import { SheetHeader, SheetSection, SheetTitle, + Textarea, } from 'ui' -import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' @@ -117,25 +117,42 @@ export function EditSecretSheet({ secret, visible, onClose }: EditSecretSheetPro description="Secrets can’t be retrieved once saved. Enter a new value to overwrite the existing value." > - -