fix: support adding multiline secrets via the UI (#27253)

**Summary**

Root cause (confirmed by reading SecretDialog.tsx): the Value field on
the User Secrets "Add secret"/"Edit secret" dialog was rendered as
<Input type="text">, a single-line HTML control, so pasting a multiline
kubeconfig/PEM/YAML got visually collapsed and the newlines were lost
before the value ever reached the form state or the API request.

Fix applied in
site/src/pages/UserSettingsPage/SecretsPage/SecretDialog.tsx:

* Swapped the Value field's <Input> for the same <Textarea> component
already used for the Description field (which correctly preserves
multiline text).
* Added rows={4} so the box is usable for multiline content, and
font-mono for readability of config-like values.
* Kept the "mask typed value" obscuring behavior but switched
-webkit-text-security:circle to :disc (Chromium's supported keyword for
<textarea>; circle is input-specific and silently no-ops on textareas).
* Removed the now-unused Input import.
* All existing onFocus/onChange/onBlur masking/saved-value logic was
preserved as-is; FormHelpers types already supported
HTMLTextAreaElement, so no type changes were needed.

Test fix in
site/src/pages/UserSettingsPage/SecretsPage/SecretsPageView.stories.tsx:
the EditSecretClearValue story's toBeVisible() assertion on the helper
text was racing the render/layout commit (this test already used a
waitFor for an analogous transient-visibility check on the neighboring
Clear button). The taller textarea shifted that timing enough to expose
the race consistently when run as part of the full suite. Wrapped the
assertion in waitFor(...) to match the existing pattern.

*Created with the help of Mux and Sonnet, tested by a human*

Working with a secret file like this:
```
➜  Downloads cat ~/.kube/config
apiVersion: v1
clusters:
  - cluster:
        certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTWpBNU16VTFOelV3SGhjTk1qUXdOekUwTURVek9UTTFXaGNOTXpRd056RXlNRFV6T1RNMQpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTWpBNU16VTFOelV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTK1JRaUlLa0RVV0tKR0Y0ZFdnbWZSVTA1Wjg1ellQSWt6aWlHUXdBZ1gKMXdyQThhNVRRbzVuYlJmeWxRQXZtZDB1SE04a05GTmZIYXJwc3lxZkZyS0dvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBmUHdjMWpnd1BraFN2Q0cwbjQ1CnZTV09ES1V3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnQ3RCRm5KR0VJVVRSZ1RRaXZFSWJIODVwQnlMdmx1RjMKQ2ZTRFhDc3hlaWtDSUF4a0JKWnJ2eUUzQzNWRWdVT0t2d2VzVUN4cFBOendpbkg5Qkd4TXJuTk4KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
        server: https://192.168.50.23:6443
    name: k3s-3-pi
```

Created via the UI in 2.35.1:
```
coder_dev=> select * from user_secrets;
-[ RECORD 1 ]+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
id           | 12853b89-a56b-408a-a9b0-11591ed04898
user_id      | 2702b6f3-b094-47ee-975e-47dc7887dbed
name         | kubeconfig
description  |
value        | apiVersion: v1 clusters:   - cluster:         certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTWpBNU16VTFOelV3SGhjTk1qUXdOekUwTURVek9UTTFXaGNOTXpRd056RXlNRFV6T1RNMQpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTWpBNU16VTFOelV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTK1JRaUlLa0RVV0tKR0Y0ZFdnbWZSVTA1Wjg1ellQSWt6aWlHUXdBZ1gKMXdyQThhNVRRbzVuYlJmeWxRQXZtZDB1SE04a05GTmZIYXJwc3lxZkZyS0dvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBmUHdjMWpnd1BraFN2Q0cwbjQ1CnZTV09ES1V3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnQ3RCRm5KR0VJVVRSZ1RRaXZFSWJIODVwQnlMdmx1RjMKQ2ZTRFhDc3hlaWtDSUF4a0JKWnJ2eUUzQzNWRWdVT0t2d2VzVUN4cFBOendpbkg5Qkd4TXJuTk4KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=         server: https://192.168.50.23:6443     name: k3s-3-pi
env_name     |
file_path    | ~/.kube/config
created_at   | 2026-07-15 15:55:07.438234+10
updated_at   | 2026-07-15 15:55:07.438234+10
value_key_id |
```

Post change:
```
coder=# select * from user_secrets ;
-[ RECORD 1 ]+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
id           | 7c4858bc-ee87-45f0-b85d-0e9b1474d69c
user_id      | 0f0a590b-0358-4a24-bb25-ccc936bcc8d6
name         | kubeconfig
description  |
value        | apiVersion: v1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          +
             | clusters:                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               +
             |   - cluster:                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            +
             |         certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTWpBNU16VTFOelV3SGhjTk1qUXdOekUwTURVek9UTTFXaGNOTXpRd056RXlNRFV6T1RNMQpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTWpBNU16VTFOelV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTK1JRaUlLa0RVV0tKR0Y0ZFdnbWZSVTA1Wjg1ellQSWt6aWlHUXdBZ1gKMXdyQThhNVRRbzVuYlJmeWxRQXZtZDB1SE04a05GTmZIYXJwc3lxZkZyS0dvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBmUHdjMWpnd1BraFN2Q0cwbjQ1CnZTV09ES1V3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnQ3RCRm5KR0VJVVRSZ1RRaXZFSWJIODVwQnlMdmx1RjMKQ2ZTRFhDc3hlaWtDSUF4a0JKWnJ2eUUzQzNWRWdVT0t2d2VzVUN4cFBOendpbkg5Qkd4TXJuTk4KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=+
             |         server: https://192.168.50.23:6443                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              +
             |     name: k3s-3-pi
env_name     |
file_path    | ~/.kube/config
created_at   | 2026-07-15 15:49:08.772126+10
updated_at   | 2026-07-15 15:53:39.375332+10
value_key_id |
```

And displayed correct in the Coder workspace:
<img width="508" height="238" alt="image"
src="https://github.com/user-attachments/assets/a0e9208e-cb73-4636-94e4-a67dcb510da4"
/>


Add secret UI is now a multiline input box:
<img width="534" height="839" alt="image"
src="https://github.com/user-attachments/assets/6e2a89a3-bef2-40c0-852e-0c9ac8e2f0cd"
/>

Co-authored-by: Atif Ali <atif@coder.com>
This commit is contained in:
Rowan Smith
2026-08-03 11:52:07 -07:00
committed by GitHub
co-authored by Atif Ali
parent 8a4adeec32
commit f44e8c12d6
2 changed files with 9 additions and 8 deletions
@@ -25,7 +25,6 @@ import {
} from "#/components/Dialog/Dialog";
import { FileUpload } from "#/components/FileUpload/FileUpload";
import { FormField } from "#/components/FormField/FormField";
import { Input } from "#/components/Input/Input";
import { Label } from "#/components/Label/Label";
import { Separator } from "#/components/Separator/Separator";
import { Spinner } from "#/components/Spinner/Spinner";
@@ -441,13 +440,13 @@ const SecretValueField: FC<SecretValueFieldProps> = ({
{required ? <RequiredFieldLabel>Value</RequiredFieldLabel> : "Value"}
</Label>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start">
<Input
<Textarea
id={field.id}
name={field.name}
type="text"
value={value}
placeholder={placeholder}
autoComplete="off"
rows={4}
aria-required={required}
aria-invalid={displayField.error}
aria-describedby={
@@ -459,9 +458,9 @@ const SecretValueField: FC<SecretValueFieldProps> = ({
}
disabled={clearValueRequested}
className={cn(
"placeholder:text-content-disabled sm:flex-1",
"placeholder:text-content-disabled sm:flex-1 font-mono",
displayField.error && "border-border-destructive",
maskTypedValue && "[-webkit-text-security:circle]",
maskTypedValue && "[-webkit-text-security:disc]",
)}
onFocus={(event) => {
if (isShowingSavedValue) {
@@ -418,9 +418,11 @@ export const EditSecretClearValue: Story = {
await user.click(dialog.getByRole("button", { name: "Clear" }));
await expect(valueField).toHaveValue("");
await expect(valueField).toBeDisabled();
await expect(
dialog.getByText("Saved value will be cleared when you update."),
).toBeVisible();
await waitFor(() =>
expect(
dialog.getByText("Saved value will be cleared when you update."),
).toBeVisible(),
);
await expect(updateButton).toBeEnabled();
await user.click(dialog.getByRole("button", { name: "Undo" }));