Add option for extra options to be required

This commit is contained in:
Andrew Baldwin 2024-11-12 15:33:56 +01:00
parent 665f786cd0
commit 83fcdda19d
8 changed files with 38 additions and 3 deletions

View File

@ -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

View File

@ -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},

View File

@ -59,15 +59,18 @@ class LocustArgumentParser(configargparse.ArgumentParser):
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 @@ class LocustArgumentParser(configargparse.ArgumentParser):
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()

View File

@ -373,6 +373,7 @@ class TestArgumentParser(LocustTestCase):
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 @@ class TestArgumentParser(LocustTestCase):
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"])

View File

@ -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):

View File

@ -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 (
<FormControlLabel
control={<Checkbox defaultChecked={defaultValue} />}
control={<Checkbox defaultChecked={defaultValue} required={isRequired} />}
label={<Markdown content={label} />}
name={name}
/>
@ -35,7 +37,14 @@ export default function CustomInput({
}
if (isSecret) {
return <PasswordField defaultValue={defaultValue} label={label} name={name} />;
return (
<PasswordField
defaultValue={defaultValue}
isRequired={isRequired}
label={label}
name={name}
/>
);
}
return (
@ -43,6 +52,7 @@ export default function CustomInput({
defaultValue={defaultValue}
label={label}
name={name}
required={isRequired}
sx={{ width: '100%' }}
type='text'
/>

View File

@ -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);
@ -28,6 +29,7 @@ export default function PasswordField({
id={`${label}-${name}-field`}
label={label}
name={name}
required={isRequired}
type={showPassword ? 'text' : 'password'}
/>
</FormControl>

View File

@ -4,4 +4,5 @@ export interface ICustomInput {
choices?: string[] | null;
defaultValue?: string | number | boolean | null;
isSecret?: boolean;
isRequired?: boolean;
}