8000 feat: allow masking workspace parameter inputs (#18595) · coder/coder@0b82f41 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0b82f41

Browse files
authored
feat: allow masking workspace parameter inputs (#18595)
1 parent d22ac1c commit 0b82f41

File tree

11 files changed

+140
-49
lines changed

11 files changed

+140
-49
lines changed

coderd/apidoc/docs.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/db2sdk/db2sdk.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ func PreviewParameter(param previewtypes.Parameter) codersdk.PreviewParameter {
816816
Placeholder: param.Styling.Placeholder,
817817
Disabled: param.Styling.Disabled,
818818
Label: param.Styling.Label,
819+
MaskInput: param.Styling.MaskInput,
819820
},
820821
Mutable: param.Mutable,
821822
DefaultValue: PreviewHCLString(param.DefaultValue),

codersdk/parameters.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ type PreviewParameterStyling struct {
9191
Placeholder *string `json:"placeholder,omitempty"`
9292
Disabled *bool `json:"disabled,omitempty"`
9393
Label *string `json:"label,omitempty"`
94+
MaskInput *bool `json:"mask_input,omitempty"`
9495
}
9596

9697
type PreviewParameterOption struct {

docs/reference/api/schemas.md

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/api/templates.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ require (
483483
require (
484484
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
485485
github.com/coder/aisdk-go v0.0.9
486-
github.com/coder/preview v1.0.1
486+
github.com/coder/preview v1.0.2
487487
github.com/fsnotify/fsnotify v1.9.0
488488
github.com/mark3labs/mcp-go v0.32.0
489489
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -916,8 +916,8 @@ github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGO
916916
github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
917917
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
918918
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
919-
github.com/coder/preview v1.0.1 h1:f6q+RjNelwnkyXfGbmVlb4dcUOQ0z4mPsb2kuQpFHuU=
920-
github.com/coder/preview v1.0.1/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM=
919+
github.com/coder/preview v1.0.2 h1:ZFfox0PgXcIouB9iWGcZyOtdL0h2a4ju1iPw/dMqsg4=
920+
github.com/coder/preview v1.0.2/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM=
921921
github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE=
922922
github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
923923
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/modules/workspaces/DynamicParameter/DynamicParameter.stories.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@ const meta: Meta<typeof DynamicParameter> = {
88
parameters: {
99
layout: "centered",
1010
},
11+
args: {
12+
parameter: MockPreviewParameter,
13+
onChange: () => {},
14+
},
1115
};
1216

1317
export default meta;
1418
type Story = StoryObj<typeof DynamicParameter>;
1519

16-
export const TextInput: Story = {
17-
args: {
18-
parameter: {
19-
...MockPreviewParameter,
20-
},
21-
},
22-
};
20+
export const TextInput: Story = {};
2321

2422
export const TextArea: Story = {
2523
args: {
@@ -230,3 +228,30 @@ export const AllBadges: Story = {
230228
isPreset: true,
231229
},
232230
};
231+
232+
export const MaskedInput: Story = {
233+
args: {
234+
parameter: {
235+
...MockPreviewParameter,
236+
form_type: "input",
237+
styling: {
238+
...MockPreviewParameter.styling,
239+
placeholder: "Tell me a secret",
240+
mask_input: true,
241+
},
242+
},
243+
},
244+
};
245+
246+
export const MaskedTextArea: Story = {
247+
args: {
248+
parameter: {
249+
...MockPreviewParameter,
250+
form_type: "textarea",
251+
styling: {
252+
...MockPreviewParameter.styling,
253+
mask_input: true,
254+
},
255+
},
256+
},
257+
};

site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
WorkspaceBuildParameter,
66
} from "api/typesGenerated";
77
import { Badge } from "components/Badge/Badge";
8+
import { Button } from "components/Button/Button";
89
import { Checkbox } from "components/Checkbox/Checkbox";
910
import { ExternalImage } from "components/ExternalImage/ExternalImage";
1011
import { Input } from "components/Input/Input";
@@ -23,6 +24,7 @@ import {
2324
SelectValue,
2425
} from "components/Select/Select";
2526
import { Slider } from "components/Slider/Slider";
27+
import { Stack } from "components/Stack/Stack";
2628
import { Switch } from "components/Switch/Switch";
2729
import { TagInput } from "components/TagInput/TagInput";
2830
import { Textarea } from "components/Textarea/Textarea";
@@ -36,13 +38,16 @@ import { useDebouncedValue } from "hooks/debounce";
3638
import { useEffectEvent } from "hooks/hookPolyfills";
3739
import {
3840
CircleAlert,
41+
Eye,
42+
EyeOff,
3943
Hourglass,
4044
Info,
4145
LinkIcon,
4246
Settings,
4347
TriangleAlert,
4448
} from "lucide-react";
4549
import { type FC, useEffect, useId, useRef, useState } from "react";
50+
import { cn } from "utils/cn";
4651
import type { AutofillBuildParameter } from "utils/richParameters";
4752
import * as Yup from "yup";
4853

@@ -265,6 +270,7 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
265270
const [localValue, setLocalValue] = useState(
266271
value !== undefined ? value : validValue(parameter.value),
267272
);
273+
const [showMaskedInput, setShowMaskedInput] = useState(false);
268274
const debouncedLocalValue = useDebouncedValue(localValue, 500);
269275
const onChangeEvent = useEffectEvent(onChange);
270276
// prevDebouncedValueRef is to prevent calling the onChangeEvent on the initial render
@@ -309,27 +315,56 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
309315
switch (parameter.form_type) {
310316
case "textarea": {
311317
return (
312-
<Textarea
313-
ref={textareaRef}
314-
id={id}
315-
className="overflow-y-auto max-h-[500px]"
316-
value={localValue}
317-
onChange={(e) => {
318-
const target = e.currentTarget;
319-
target.style.height = "auto";
320-
target.style.height = `${target.scrollHeight}px`;
321-
322-
setLocalValue(e.target.value);
323-
}}
324-
disabled={disabled}
325-
placeholder={parameter.styling?.placeholder}
326-
required={parameter.required}
327-
/>
318+
<Stack direction="row" spacing={0} alignItems="center">
319+
<Textarea
320+
ref={textareaRef}
321+
id={id}
322+
className={cn(
323+
"overflow-y-auto max-h-[500px]",
324+
parameter.styling?.mask_input &&
325+
!showMaskedInput &&
326+
"[-webkit-text-security:disc]",
327+
)}
328+
value={localValue}
329+
onChange={(e) => {
330+
const target = e.currentTarget;
331+
target.style.height = "auto";
332+
target.style.height = `${target.scrollHeight}px`;
333+
334+
setLocalValue(e.target.value);
335+
}}
336+
disabled={disabled}
337+
placeholder={parameter.styling?.placeholder}
338+
required={parameter.required}
339+
/>
340+
{parameter.styling?.mask_input && (
341+
<Button
342+
type="button"
343+
variant="subtle"
344+
size="icon"
345+
onMouseDown={() => setShowMaskedInput(true)}
346+
onMouseOut={() => setShowMaskedInput(false)}
347+
onMouseUp={() => setShowMaskedInput(false)}
348+
disabled={disabled}
349+
>
350+
{showMaskedInput ? (
351+
<EyeOff className="h-4 w-4" />
352+
) : (
353+
<Eye className="h-4 w-4" />
354+
)}
355+
</Button>
356+
)}
357+
</Stack>
328358
);
329359
}
330360

331361
case "input": {
332-
const inputType = parameter.type === "number" ? "number" : "text";
362+
const inputType =
363+
parameter.type === "number"
364+
? "number"
365+
: parameter.styling?.mask_input && !showMaskedInput
366+
? "password"
367+
: "text";
333368
const inputProps: Record<string, unknown> = {};
334369

335370
if (parameter.type === "number") {
@@ -346,18 +381,37 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
346381
}
347382

348383
return (
349-
<Input
350-
id={id}
351-
type={inputType}
352-
value={localValue}
353-
onChange={(e) => {
354-
setLocalValue(e.target.value);
355-
}}
356-
disabled={disabled}
357-
required={parameter.required}
358-
placeholder={parameter.styling?.placeholder}
359-
{...inputProps}
360-
/>
384+
<Stack direction="row" spacing={0} alignItems="center">
385+
<Input
386+
id={id}
387+
type={inputType}
388+
value={localValue}
389+
onChange={(e) => {
390+
setLocalValue(e.target.value);
391+
}}
392+
disabled={disabled}
393+
required={parameter.required}
394+
placeholder={parameter.styling?.placeholder}
395+
{...inputProps}
396+
/>
397+
{parameter.styling?.mask_input && parameter.type !== "number" && (
398+
<Button
399+
type="button"
400+
variant="subtle"
401+
size="icon"
402+
onMouseDown={() => setShowMaskedInput(true)}
403+
onMouseOut={() => setShowMaskedInput(false)}
404+
onMouseUp={() => setShowMaskedInput(false)}
405+
disabled={disabled}
406+
>
407+
{showMaskedInput ? (
408+
<EyeOff className="h-4 w-4" />
409+
) : (
410+
<Eye className="h-4 w-4" />
411+
)}
412+
</Button>
413+
)}
414+
</Stack>
361415
);
362416
}
363417
}
@@ -451,9 +505,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
451505

452506
return (
453507
<MultiSelectCombobox
454-
inputProps={{
455-
id: id,
456-
}}
508+
inputProps={{ id }}
457509
options={options}
458510
defaultOptions={selectedOptions}
459511
onChange={(newValues) => {
@@ -698,7 +750,7 @@ const isValidParameterOption = (
698750
if (Array.isArray(parsed)) {
699751
values = parsed;
700752
}
701-
} catch (e) {
753+
} catch {
702754
return false;
703755
}
704756

@@ -899,12 +951,12 @@ export const Diagnostics: FC<DiagnosticsProps> = ({ diagnostics }) => {
899951
{diagnostics.map((diagnostic, index) => (
900952
<div
901953
key={`diagnostic-${diagnostic.summary}-${index}`}
902-
className={`text-xs font-semibold flex flex-col rounded-md border px-3.5 py-3.5 border-solid
903-
${
904-
diagnostic.severity === "error"
905-
? "text-content-primary border-border-destructive bg-content-destructive/15"
906-
: " 725B text-content-primary border-border-warning bg-content-warning/15"
907-
}`}
954+
className={cn(
955+
"text-xs font-semibold flex flex-col rounded-md border px-3.5 py-3.5 border-solid",
956+
diagnostic.severity === "error"
957+
? "text-content-primary border-border-destructive bg-content-destructive/15"
958+
: "text-content-primary border-border-warning bg-content-warning/15",
959+
)}
908960
>
909961
<div className="flex flex-row items-start">
910962
{diagnostic.severity === "error" && (

0 commit comments

Comments
 (0)
0