Skip to content

Commit

Permalink
Merge pull request #2981 from andrewbaldwin44/feature/required-exta-o…
Browse files Browse the repository at this point in the history
…ptions

Add option for Extra Options to be Required
  • Loading branch information
cyberw authored Nov 12, 2024
2 parents 6b40e26 + 83fcdda commit c8fab32
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 3 deletions.
4 changes: 4 additions & 0 deletions examples/add_command_line_argument.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ def _(parser):
parser.add_argument("--my-ui-invisible-argument", include_in_web_ui=False, default="I am invisible")
# Set `is_secret` to True if you want the text input to be password masked in the web UI
parser.add_argument("--my-ui-password-argument", is_secret=True, default="I am a secret")
# Use a boolean default value if you want the input to be a checkmark
parser.add_argument("--my-ui-boolean-argument", default=True)
# Set `is_required` to mark a form field as required
parser.add_argument("--my-ui-required-argument", is_required=True, default="I am required")


@events.test_start.add_listener
Expand Down
2 changes: 2 additions & 0 deletions examples/web_ui_auth/custom_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def locust_init(environment, **_kwargs):
{
"label": "Username",
"name": "username",
# make field required
"is_required": True,
},
# boolean checkmark field
{"label": "Admin", "name": "is_admin", "default_value": False},
Expand Down
13 changes: 13 additions & 0 deletions locust/argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,18 @@ def add_argument(self, *args, **kwargs) -> configargparse.Action:
Arguments:
include_in_web_ui: If True (default), the argument will show in the UI.
is_secret: If True (default is False) and include_in_web_ui is True, the argument will show in the UI with a password masked text input.
is_required: If True (default is False) and include_in_web_ui is True, the argument will show in the UI as a required form field.
Returns:
argparse.Action: the new argparse action
"""
include_in_web_ui = kwargs.pop("include_in_web_ui", True)
is_secret = kwargs.pop("is_secret", False)
is_required = kwargs.pop("is_required", False)
action = super().add_argument(*args, **kwargs)
action.include_in_web_ui = include_in_web_ui
action.is_secret = is_secret
action.is_required = is_required
return action

@property
Expand All @@ -82,6 +85,14 @@ def secret_args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
if a.dest in self.args_included_in_web_ui and hasattr(a, "is_secret") and a.is_secret
}

@property
def required_args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
return {
a.dest: a
for a in self._actions
if a.dest in self.args_included_in_web_ui and hasattr(a, "is_required") and a.is_required
}


class LocustTomlConfigParser(configargparse.TomlConfigParser):
def parse(self, stream):
Expand Down Expand Up @@ -798,6 +809,7 @@ def default_args_dict() -> dict:
class UIExtraArgOptions(NamedTuple):
default_value: str
is_secret: bool
is_required: bool
help_text: str
choices: list[str] | None = None

Expand All @@ -813,6 +825,7 @@ def ui_extra_args_dict(args=None) -> dict[str, dict[str, Any]]:
k: UIExtraArgOptions(
default_value=v,
is_secret=k in parser.secret_args_included_in_web_ui,
is_required=k in parser.required_args_included_in_web_ui,
help_text=parser.args_included_in_web_ui[k].help,
choices=parser.args_included_in_web_ui[k].choices,
)._asdict()
Expand Down
2 changes: 2 additions & 0 deletions locust/test/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ def _(parser, **kw):
parser.add_argument("--a1", help="a1 help")
parser.add_argument("--a2", help="a2 help", include_in_web_ui=False)
parser.add_argument("--a3", help="a3 help", is_secret=True)
parser.add_argument("--a4", help="a3 help", is_required=True)

args = ["-u", "666", "--a1", "v1", "--a2", "v2", "--a3", "v3"]
options = parse_options(args=args)
Expand All @@ -384,6 +385,7 @@ def _(parser, **kw):
self.assertIn("a1", extra_args)
self.assertNotIn("a2", extra_args)
self.assertIn("a3", extra_args)
self.assertIn("a4", extra_args)
self.assertEqual("v1", extra_args["a1"]["default_value"])


Expand Down
1 change: 1 addition & 0 deletions locust/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class InputField(TypedDict, total=False):
default_value: bool | None
choices: list[str] | None
is_secret: bool | None
is_required: bool | None


class CustomForm(TypedDict, total=False):
Expand Down
14 changes: 12 additions & 2 deletions locust/webui/src/components/Form/CustomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function CustomInput({
defaultValue,
choices,
isSecret,
isRequired,
}: ICustomInput) {
if (choices) {
return (
Expand All @@ -19,6 +20,7 @@ export default function CustomInput({
label={label}
name={name}
options={choices}
required={isRequired}
sx={{ width: '100%' }}
/>
);
Expand All @@ -27,22 +29,30 @@ export default function CustomInput({
if (typeof defaultValue === 'boolean') {
return (
<FormControlLabel
control={<Checkbox defaultChecked={defaultValue} />}
control={<Checkbox defaultChecked={defaultValue} required={isRequired} />}
label={<Markdown content={label} />}
name={name}
/>
);
}

if (isSecret) {
return <PasswordField defaultValue={defaultValue} label={label} name={name} />;
return (
<PasswordField
defaultValue={defaultValue}
isRequired={isRequired}
label={label}
name={name}
/>
);
}

return (
<TextField
defaultValue={defaultValue}
label={label}
name={name}
required={isRequired}
sx={{ width: '100%' }}
type='text'
/>
Expand Down
4 changes: 3 additions & 1 deletion locust/webui/src/components/Form/PasswordField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export default function PasswordField({
name = 'password',
label = 'Password',
defaultValue,
}: Partial<Pick<ICustomInput, 'name' | 'label' | 'defaultValue'>>) {
isRequired,
}: Partial<Pick<ICustomInput, 'name' | 'label' | 'defaultValue' | 'isRequired'>>) {
const [showPassword, setShowPassword] = useState(false);

const handleClickShowPassword = () => setShowPassword(!showPassword);
Expand All @@ -28,6 +29,7 @@ export default function PasswordField({
id={`${label}-${name}-field`}
label={label}
name={name}
required={isRequired}
type={showPassword ? 'text' : 'password'}
/>
</FormControl>
Expand Down
1 change: 1 addition & 0 deletions locust/webui/src/types/form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface ICustomInput {
choices?: string[] | null;
defaultValue?: string | number | boolean | null;
isSecret?: boolean;
isRequired?: boolean;
}

0 comments on commit c8fab32

Please sign in to comment.