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;
}