mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow masking workspace parameter inputs (#18595)
This commit is contained in:
Generated
+3
@@ -14176,6 +14176,9 @@ const docTemplate = `{
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"mask_input": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
Generated
+3
@@ -12834,6 +12834,9 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"mask_input": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -816,6 +816,7 @@ func PreviewParameter(param previewtypes.Parameter) codersdk.PreviewParameter {
|
||||
Placeholder: param.Styling.Placeholder,
|
||||
Disabled: param.Styling.Disabled,
|
||||
Label: param.Styling.Label,
|
||||
MaskInput: param.Styling.MaskInput,
|
||||
},
|
||||
Mutable: param.Mutable,
|
||||
DefaultValue: PreviewHCLString(param.DefaultValue),
|
||||
|
||||
@@ -91,6 +91,7 @@ type PreviewParameterStyling struct {
|
||||
Placeholder *string `json:"placeholder,omitempty"`
|
||||
Disabled *bool `json:"disabled,omitempty"`
|
||||
Label *string `json:"label,omitempty"`
|
||||
MaskInput *bool `json:"mask_input,omitempty"`
|
||||
}
|
||||
|
||||
type PreviewParameterOption struct {
|
||||
|
||||
Generated
+4
@@ -2890,6 +2890,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"styling": {
|
||||
"disabled": true,
|
||||
"label": "string",
|
||||
"mask_input": true,
|
||||
"placeholder": "string"
|
||||
},
|
||||
"type": "string",
|
||||
@@ -5059,6 +5060,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
"styling": {
|
||||
"disabled": true,
|
||||
"label": "string",
|
||||
"mask_input": true,
|
||||
"placeholder": "string"
|
||||
},
|
||||
"type": "string",
|
||||
@@ -5128,6 +5130,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
{
|
||||
"disabled": true,
|
||||
"label": "string",
|
||||
"mask_input": true,
|
||||
"placeholder": "string"
|
||||
}
|
||||
```
|
||||
@@ -5138,6 +5141,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
|---------------|---------|----------|--------------|-------------|
|
||||
| `disabled` | boolean | false | | |
|
||||
| `label` | string | false | | |
|
||||
| `mask_input` | boolean | false | | |
|
||||
| `placeholder` | string | false | | |
|
||||
|
||||
## codersdk.PreviewParameterValidation
|
||||
|
||||
Generated
+1
@@ -2697,6 +2697,7 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/
|
||||
"styling": {
|
||||
"disabled": true,
|
||||
"label": "string",
|
||||
"mask_input": true,
|
||||
"placeholder": "string"
|
||||
},
|
||||
"type": "string",
|
||||
|
||||
@@ -483,7 +483,7 @@ require (
|
||||
require (
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
github.com/coder/aisdk-go v0.0.9
|
||||
github.com/coder/preview v1.0.1
|
||||
github.com/coder/preview v1.0.2
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/mark3labs/mcp-go v0.32.0
|
||||
)
|
||||
|
||||
@@ -916,8 +916,8 @@ github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGO
|
||||
github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
|
||||
github.com/coder/preview v1.0.1 h1:f6q+RjNelwnkyXfGbmVlb4dcUOQ0z4mPsb2kuQpFHuU=
|
||||
github.com/coder/preview v1.0.1/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM=
|
||||
github.com/coder/preview v1.0.2 h1:ZFfox0PgXcIouB9iWGcZyOtdL0h2a4ju1iPw/dMqsg4=
|
||||
github.com/coder/preview v1.0.2/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM=
|
||||
github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE=
|
||||
github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
|
||||
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
|
||||
|
||||
Generated
+1
@@ -1836,6 +1836,7 @@ export interface PreviewParameterStyling {
|
||||
readonly placeholder?: string;
|
||||
readonly disabled?: boolean;
|
||||
readonly label?: string;
|
||||
readonly mask_input?: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/parameters.go
|
||||
|
||||
@@ -8,18 +8,16 @@ const meta: Meta<typeof DynamicParameter> = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
args: {
|
||||
parameter: MockPreviewParameter,
|
||||
onChange: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DynamicParameter>;
|
||||
|
||||
export const TextInput: Story = {
|
||||
args: {
|
||||
parameter: {
|
||||
...MockPreviewParameter,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const TextInput: Story = {};
|
||||
|
||||
export const TextArea: Story = {
|
||||
args: {
|
||||
@@ -230,3 +228,30 @@ export const AllBadges: Story = {
|
||||
isPreset: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MaskedInput: Story = {
|
||||
args: {
|
||||
parameter: {
|
||||
...MockPreviewParameter,
|
||||
form_type: "input",
|
||||
styling: {
|
||||
...MockPreviewParameter.styling,
|
||||
placeholder: "Tell me a secret",
|
||||
mask_input: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MaskedTextArea: Story = {
|
||||
args: {
|
||||
parameter: {
|
||||
...MockPreviewParameter,
|
||||
form_type: "textarea",
|
||||
styling: {
|
||||
...MockPreviewParameter.styling,
|
||||
mask_input: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Checkbox } from "components/Checkbox/Checkbox";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { Input } from "components/Input/Input";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
SelectValue,
|
||||
} from "components/Select/Select";
|
||||
import { Slider } from "components/Slider/Slider";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { Switch } from "components/Switch/Switch";
|
||||
import { TagInput } from "components/TagInput/TagInput";
|
||||
import { Textarea } from "components/Textarea/Textarea";
|
||||
@@ -36,6 +38,8 @@ import { useDebouncedValue } from "hooks/debounce";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import {
|
||||
CircleAlert,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Hourglass,
|
||||
Info,
|
||||
LinkIcon,
|
||||
@@ -43,6 +47,7 @@ import {
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import { type FC, useEffect, useId, useRef, useState } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import type { AutofillBuildParameter } from "utils/richParameters";
|
||||
import * as Yup from "yup";
|
||||
|
||||
@@ -265,6 +270,7 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
||||
const [localValue, setLocalValue] = useState(
|
||||
value !== undefined ? value : validValue(parameter.value),
|
||||
);
|
||||
const [showMaskedInput, setShowMaskedInput] = useState(false);
|
||||
const debouncedLocalValue = useDebouncedValue(localValue, 500);
|
||||
const onChangeEvent = useEffectEvent(onChange);
|
||||
// prevDebouncedValueRef is to prevent calling the onChangeEvent on the initial render
|
||||
@@ -309,27 +315,56 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
||||
switch (parameter.form_type) {
|
||||
case "textarea": {
|
||||
return (
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
id={id}
|
||||
className="overflow-y-auto max-h-[500px]"
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
const target = e.currentTarget;
|
||||
target.style.height = "auto";
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
<Stack direction="row" spacing={0} alignItems="center">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
id={id}
|
||||
className={cn(
|
||||
"overflow-y-auto max-h-[500px]",
|
||||
parameter.styling?.mask_input &&
|
||||
!showMaskedInput &&
|
||||
"[-webkit-text-security:disc]",
|
||||
)}
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
const target = e.currentTarget;
|
||||
target.style.height = "auto";
|
||||
target.style.height = `${target.scrollHeight}px`;
|
||||
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
required={parameter.required}
|
||||
/>
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
required={parameter.required}
|
||||
/>
|
||||
{parameter.styling?.mask_input && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
onMouseDown={() => setShowMaskedInput(true)}
|
||||
onMouseOut={() => setShowMaskedInput(false)}
|
||||
onMouseUp={() => setShowMaskedInput(false)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{showMaskedInput ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
case "input": {
|
||||
const inputType = parameter.type === "number" ? "number" : "text";
|
||||
const inputType =
|
||||
parameter.type === "number"
|
||||
? "number"
|
||||
: parameter.styling?.mask_input && !showMaskedInput
|
||||
? "password"
|
||||
: "text";
|
||||
const inputProps: Record<string, unknown> = {};
|
||||
|
||||
if (parameter.type === "number") {
|
||||
@@ -346,18 +381,37 @@ const DebouncedParameterField: FC<DebouncedParameterFieldProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
type={inputType}
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
required={parameter.required}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
<Stack direction="row" spacing={0} alignItems="center">
|
||||
<Input
|
||||
id={id}
|
||||
type={inputType}
|
||||
value={localValue}
|
||||
onChange={(e) => {
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
required={parameter.required}
|
||||
placeholder={parameter.styling?.placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
{parameter.styling?.mask_input && parameter.type !== "number" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
onMouseDown={() => setShowMaskedInput(true)}
|
||||
onMouseOut={() => setShowMaskedInput(false)}
|
||||
onMouseUp={() => setShowMaskedInput(false)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{showMaskedInput ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -451,9 +505,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
|
||||
return (
|
||||
<MultiSelectCombobox
|
||||
inputProps={{
|
||||
id: id,
|
||||
}}
|
||||
inputProps={{ id }}
|
||||
options={options}
|
||||
defaultOptions={selectedOptions}
|
||||
onChange={(newValues) => {
|
||||
@@ -698,7 +750,7 @@ const isValidParameterOption = (
|
||||
if (Array.isArray(parsed)) {
|
||||
values = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -899,12 +951,12 @@ export const Diagnostics: FC<DiagnosticsProps> = ({ diagnostics }) => {
|
||||
{diagnostics.map((diagnostic, index) => (
|
||||
<div
|
||||
key={`diagnostic-${diagnostic.summary}-${index}`}
|
||||
className={`text-xs font-semibold flex flex-col rounded-md border px-3.5 py-3.5 border-solid
|
||||
${
|
||||
diagnostic.severity === "error"
|
||||
? "text-content-primary border-border-destructive bg-content-destructive/15"
|
||||
: "text-content-primary border-border-warning bg-content-warning/15"
|
||||
}`}
|
||||
className={cn(
|
||||
"text-xs font-semibold flex flex-col rounded-md border px-3.5 py-3.5 border-solid",
|
||||
diagnostic.severity === "error"
|
||||
? "text-content-primary border-border-destructive bg-content-destructive/15"
|
||||
: "text-content-primary border-border-warning bg-content-warning/15",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-start">
|
||||
{diagnostic.severity === "error" && (
|
||||
|
||||
Reference in New Issue
Block a user