From 83fcdda19d2ccf6af68e5b3b853963bc629a5148 Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Tue, 12 Nov 2024 15:33:56 +0100 Subject: [PATCH] Add option for extra options to be required --- examples/add_command_line_argument.py | 4 ++++ examples/web_ui_auth/custom_form.py | 2 ++ locust/argument_parser.py | 13 +++++++++++++ locust/test/test_parser.py | 2 ++ locust/web.py | 1 + locust/webui/src/components/Form/CustomInput.tsx | 14 ++++++++++++-- locust/webui/src/components/Form/PasswordField.tsx | 4 +++- locust/webui/src/types/form.types.ts | 1 + 8 files changed, 38 insertions(+), 3 deletions(-) diff --git a/examples/add_command_line_argument.py b/examples/add_command_line_argument.py index 315e32ad4d..f0010b01fc 100644 --- a/examples/add_command_line_argument.py +++ b/examples/add_command_line_argument.py @@ -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 diff --git a/examples/web_ui_auth/custom_form.py b/examples/web_ui_auth/custom_form.py index 2b29accdfd..0345aa5fee 100644 --- a/examples/web_ui_auth/custom_form.py +++ b/examples/web_ui_auth/custom_form.py @@ -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}, diff --git a/locust/argument_parser.py b/locust/argument_parser.py index 70705cc130..39aaf89526 100644 --- a/locust/argument_parser.py +++ b/locust/argument_parser.py @@ -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 @@ -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): @@ -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 @@ -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() diff --git a/locust/test/test_parser.py b/locust/test/test_parser.py index a76cc9d7fb..e50d581c39 100644 --- a/locust/test/test_parser.py +++ b/locust/test/test_parser.py @@ -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) @@ -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"]) diff --git a/locust/web.py b/locust/web.py index 2705cf8a1e..8c18a4c0f5 100644 --- a/locust/web.py +++ b/locust/web.py @@ -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): diff --git a/locust/webui/src/components/Form/CustomInput.tsx b/locust/webui/src/components/Form/CustomInput.tsx index 40810e5a4d..66c32968a6 100644 --- a/locust/webui/src/components/Form/CustomInput.tsx +++ b/locust/webui/src/components/Form/CustomInput.tsx @@ -11,6 +11,7 @@ export default function CustomInput({ defaultValue, choices, isSecret, + isRequired, }: ICustomInput) { if (choices) { return ( @@ -19,6 +20,7 @@ export default function CustomInput({ label={label} name={name} options={choices} + required={isRequired} sx={{ width: '100%' }} /> ); @@ -27,7 +29,7 @@ export default function CustomInput({ if (typeof defaultValue === 'boolean') { return ( } + control={} label={} name={name} /> @@ -35,7 +37,14 @@ export default function CustomInput({ } if (isSecret) { - return ; + return ( + + ); } return ( @@ -43,6 +52,7 @@ export default function CustomInput({ defaultValue={defaultValue} label={label} name={name} + required={isRequired} sx={{ width: '100%' }} type='text' /> diff --git a/locust/webui/src/components/Form/PasswordField.tsx b/locust/webui/src/components/Form/PasswordField.tsx index 93f5f3d382..715f2dd965 100644 --- a/locust/webui/src/components/Form/PasswordField.tsx +++ b/locust/webui/src/components/Form/PasswordField.tsx @@ -8,7 +8,8 @@ export default function PasswordField({ name = 'password', label = 'Password', defaultValue, -}: Partial>) { + isRequired, +}: Partial>) { const [showPassword, setShowPassword] = useState(false); const handleClickShowPassword = () => setShowPassword(!showPassword); @@ -28,6 +29,7 @@ export default function PasswordField({ id={`${label}-${name}-field`} label={label} name={name} + required={isRequired} type={showPassword ? 'text' : 'password'} /> diff --git a/locust/webui/src/types/form.types.ts b/locust/webui/src/types/form.types.ts index b533092113..60166e6ebf 100644 --- a/locust/webui/src/types/form.types.ts +++ b/locust/webui/src/types/form.types.ts @@ -4,4 +4,5 @@ export interface ICustomInput { choices?: string[] | null; defaultValue?: string | number | boolean | null; isSecret?: boolean; + isRequired?: boolean; }