Skip to content

Commit

Permalink
Improvements to workflow parameter validators.
Browse files Browse the repository at this point in the history
- Allow specifying a min/max for integer and float parameters.
- Swap language from regex to "Regular Expression" and synchronize help with regular expression fields in rule builder.
- Extend regular expression help.
- Improve default message for in_range validator. Probably a reversion in some ways introduced with galaxyproject#19027.
  • Loading branch information
jmchilton committed Nov 7, 2024
1 parent 8a8fa85 commit 1f1c118
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 26 deletions.
11 changes: 10 additions & 1 deletion client/src/components/Form/FormElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import FormSelection from "./Elements/FormSelection.vue";
import FormTags from "./Elements/FormTags.vue";
import FormText from "./Elements/FormText.vue";
import FormUpload from "./Elements/FormUpload.vue";
import FormElementHelpMarkdown from "./FormElementHelpMarkdown.vue";
interface FormElementProps {
id?: string;
Expand All @@ -35,6 +36,7 @@ interface FormElementProps {
title?: string;
refreshOnChange?: boolean;
help?: string;
helpFormat?: string;
error?: string;
warning?: string;
disabled?: boolean;
Expand Down Expand Up @@ -63,6 +65,7 @@ const props = withDefaults(defineProps<FormElementProps>(), {
connectedDisableText: "Add connection to module.",
connectedEnableIcon: "fa fa-times",
connectedDisableIcon: "fa fa-arrows-alt-h",
helpFormat: "html",
workflowBuildingMode: false,
});
Expand Down Expand Up @@ -337,7 +340,13 @@ function onAlert(value: string | undefined) {
</div>

<div v-if="showPreview" class="ui-form-preview pt-1 pl-2 mt-1">{{ previewText }}</div>
<span v-if="Boolean(helpText)" class="ui-form-info form-text text-muted" v-html="helpText" />
<span
v-if="Boolean(helpText) && helpFormat != 'markdown'"
class="ui-form-info form-text text-muted"
v-html="helpText" />
<span v-else-if="Boolean(helpText)" class="ui-form-info form-text text-muted"
><FormElementHelpMarkdown :content="helpText"
/></span>
</div>
</template>

Expand Down
56 changes: 56 additions & 0 deletions client/src/components/Form/FormElementHelpMarkdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { markup } from "@/components/ObjectStore/configurationMarkdown";
import { getAppRoot } from "@/onload/loadConfig";
import HelpPopover from "@/components/Help/HelpPopover.vue";
const props = defineProps<{
content: string;
}>();
const markdownHtml = computed(() => markup(props.content ?? "", false));
const helpHtml = ref<HTMLDivElement>();
interface InternalTypeReference {
element: HTMLElement;
term: string;
}
const internalHelpReferences = ref<InternalTypeReference[]>([]);
function setupPopovers() {
internalHelpReferences.value.length = 0;
if (helpHtml.value) {
const links = helpHtml.value.getElementsByTagName("a");
Array.from(links).forEach((link) => {
if (link.href.startsWith("gxhelp://")) {
const uri = link.href.substr("gxhelp://".length);
internalHelpReferences.value.push({ element: link, term: uri });
link.href = `${getAppRoot()}help/terms/${uri}`;
link.style.color = "inherit";
link.style.textDecorationLine = "underline";
link.style.textDecorationStyle = "dashed";
}
});
}
}
onMounted(setupPopovers);
</script>

<template>
<span>
<!-- Disable v-html warning because we allow markdown generated HTML
in various places in the Galaxy interface. Raw HTML is not allowed
here because admin = false in the call to markup.
-->
<!-- eslint-disable-next-line vue/no-v-html -->
<div ref="helpHtml" v-html="markdownHtml" />
<span v-for="(value, i) in internalHelpReferences" :key="i">
<HelpPopover :target="value.element" :term="value.term" />
</span>
</span>
</template>
2 changes: 2 additions & 0 deletions client/src/components/Form/FormInputs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
v-model="input.test_param.value"
:type="input.test_param.type"
:help="input.test_param.help"
:help-format="input.test_param.help_format"
:refresh-on-change="false"
:disabled="sustainConditionals"
:attributes="input.test_param"
Expand Down Expand Up @@ -51,6 +52,7 @@
:error="input.error"
:warning="input.warning"
:help="input.help"
:help-format="input.help_format"
:refresh-on-change="input.refresh_on_change"
:attributes="input.attributes || input"
:collapsed-enable-text="collapsedEnableText"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Help/HelpPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defineProps<Props>();
</script>

<template>
<BPopover :target="target" triggers="hover" placement="bottom">
<BPopover v-if="target" :target="target" triggers="hover" placement="bottom">
<HelpTerm :term="term" />
</BPopover>
</template>
5 changes: 5 additions & 0 deletions client/src/components/Help/terms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ unix:
debug what is wrong with the execution of an application.
More information on stack traces can be found on [Wikipedia](https://en.wikipedia.org/wiki/Stack_trace).
programming:
python:
regex: |
Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in [this Python for Biologists tutorial](https://pythonforbiologists.com/tutorial/regex.html).
The website [regex101](https://regex101.com/) is a useful playground to explore regular expressions (be sure to enable "Python" as your flavor) and language models such as ChatGPT can help interactively build up and explain regular expressions from natural language prompts or examples.
galaxy:
collections:
flatList: |
Expand Down
12 changes: 5 additions & 7 deletions client/src/components/RuleBuilder/RegularExpressionInput.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<label v-b-tooltip.hover for="regular_expression" :title="title">{{ label }}</label>
<span v-b-popover.html="popoverContent" :title="popoverTitle" class="fa fa-question"></span>
<label ref="helpTarget" v-b-tooltip.hover for="regular_expression">{{ label }}</label>
<HelpPopover :target="$refs.helpTarget" term="programming.python.regex" />
<input
v-b-tooltip.hover.left
:title="title"
Expand All @@ -16,7 +16,10 @@
<script>
import _l from "utils/localization";
import HelpPopover from "@/components/Help/HelpPopover.vue";
export default {
components: { HelpPopover },
props: {
target: {
required: true,
Expand All @@ -32,11 +35,6 @@ export default {
popoverTitle() {
return _l("Regular Expressions");
},
popoverContent() {
return _l(
`Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in <a href="https://pythonforbiologists.com/tutorial/regex.html">this Python for Biologists tutorial</a>.`
);
},
},
};
</script>
6 changes: 4 additions & 2 deletions lib/galaxy/tool_util/parser/parameter_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ def default_message(self) -> str:
op1 = "<"
if self.exclude_max:
op2 = "<"
range_description_str = f"({self.min} {op1} value {op2} {self.max})"
min_str = str(self.min) if self.min is not None else "-infinity"
max_str = str(self.max) if self.max is not None else "+infinity"
range_description_str = f"({min_str} {op1} value {op2} {max_str})"
return f"Value ('%s') must {'not ' if self.negate else ''}fulfill {range_description_str}"


Expand Down Expand Up @@ -466,7 +468,7 @@ def default_message(self) -> str:
]


DiscriminatedAnyValidatorModel = TypeAdapter(AnyValidatorModel)
DiscriminatedAnyValidatorModel = TypeAdapter(AnyValidatorModel) # type:ignore[var-annotated]


def parse_dict_validators(validator_dicts: List[Dict[str, Any]], trusted: bool) -> List[AnyValidatorModel]:
Expand Down
3 changes: 2 additions & 1 deletion lib/galaxy/tools/parameters/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class ToolParameter(UsesDictVisibleKeys):
"""

name: str
dict_collection_visible_keys = ["name", "argument", "type", "label", "help", "refresh_on_change"]
dict_collection_visible_keys = ["name", "argument", "type", "label", "help", "help_format", "refresh_on_change"]

def __init__(self, tool, input_source, context=None):
input_source = ensure_input_source(input_source)
Expand All @@ -193,6 +193,7 @@ def __init__(self, tool, input_source, context=None):
self.is_dynamic = False
self.label = input_source.parse_label()
self.help = input_source.parse_help()
self.help_format = input_source.get("help_format") or "html"
if (sanitizer_elem := input_source.parse_sanitizer_elem()) is not None:
self.sanitizer = ToolParameterSanitizer.from_element(sanitizer_elem)
else:
Expand Down
86 changes: 72 additions & 14 deletions lib/galaxy/workflow/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@
BooleanToolParameter,
DataCollectionToolParameter,
DataToolParameter,
FloatToolParameter,
HiddenToolParameter,
IntegerToolParameter,
parameter_types,
raw_to_galaxy,
SelectToolParameter,
Expand Down Expand Up @@ -1282,7 +1284,7 @@ def get_inputs(self):
specify_multiple = BooleanToolParameter(None, specify_multiple_source)

add_validators_repeat = Repeat("validators")
add_validators_repeat._title = "Add validator to restrict valid input"
add_validators_repeat.title = "Add validator to restrict valid input"
add_validators_repeat.min = 0
add_validators_repeat.max = math.inf
add_validators_repeat.inputs = {
Expand All @@ -1291,8 +1293,9 @@ def get_inputs(self):
{
"optional": False,
"name": "regex_match",
"label": "Specify regex",
"help": "Provided regex must match input value for input to be valid",
"label": "Specify regular expression",
"help": "Provided [regular expression](gxhelp://programming.python.reg.ex) must match input value for input to be valid",
"help_format": "markdown",
},
),
"regex_doc": TextToolParameter(
Expand All @@ -1301,7 +1304,7 @@ def get_inputs(self):
"optional": False,
"name": "regex_doc",
"label": "Specify a message",
"help": "This message will be shown if the regex does not match the input",
"help": "This message will be shown if the regular does not match the input",
},
),
}
Expand Down Expand Up @@ -1389,6 +1392,45 @@ def get_inputs(self):
restrictions_cond.cases = restrictions_cond_cases
when_this_type.inputs["restrictions"] = restrictions_cond

if param_type == "integer":
when_this_type.inputs["min"] = IntegerToolParameter(
None,
{
"name": "min",
"optional": True,
"value": parameter_def.get("min", ""),
"label": "Set a minimum value for this input",
},
)
when_this_type.inputs["max"] = IntegerToolParameter(
None,
{
"name": "max",
"optional": True,
"value": parameter_def.get("max", ""),
"label": "Set a maximum value for this input",
},
)
if param_type == "float":
when_this_type.inputs["min"] = FloatToolParameter(
None,
{
"name": "min",
"optional": True,
"value": parameter_def.get("min", ""),
"label": "Set a minimum value for this input",
},
)
when_this_type.inputs["max"] = FloatToolParameter(
None,
{
"name": "max",
"optional": True,
"value": parameter_def.get("max", ""),
"label": "Set a maximum value for this input",
},
)

cases.append(when_this_type)

parameter_type_cond.cases = cases
Expand Down Expand Up @@ -1503,7 +1545,7 @@ def _parameter_def_list_to_options(parameter_value):
parameter_kwds["options"] = _parameter_def_list_to_options(restriction_values)
restricted_inputs = True

if is_text and parameter_def.get("validators"):
if parameter_def.get("validators"):
parameter_kwds["validators"] = parameter_def["validators"]

client_parameter_type = parameter_type
Expand Down Expand Up @@ -1614,13 +1656,17 @@ def step_state_to_tool_state(self, state):
# the form definition can change from Galaxy to Galaxy fairly freely, but the source validators are persisted
# and need to be consistent - here we convert the persisted/YAML tool definition version to the "tool form" version.
for i, source_validator in enumerate(source_validators):
form_validators.append(
{
"__index__": i,
"regex_doc": source_validator.get("message"),
"regex_match": source_validator.get("expression"),
}
)
source_type = source_validator["type"]
if source_type == "regex":
form_validators.append(
{
"regex_doc": source_validator.get("message"),
"regex_match": source_validator.get("expression"),
}
)
elif source_type == "in_range":
state["parameter_definition"]["min"] = source_validator.get("min")
state["parameter_definition"]["max"] = source_validator.get("max")
state["parameter_definition"]["validators"] = form_validators
state["parameter_definition"]["restrictions"] = {}
state["parameter_definition"]["restrictions"]["how"] = restrictions_how
Expand Down Expand Up @@ -1665,9 +1711,21 @@ def _parse_state_into_dict(self):
optional = False
if "multiple" in parameters_def:
rval["multiple"] = parameters_def["multiple"]
source_validators = []
if "min" in parameters_def or "max" in parameters_def:
min = parameters_def.get("min")
max = parameters_def.get("max")
source_validators.append(
{
"min": min,
"max": max,
"negate": False,
"type": "in_range",
}
)

if "validators" in parameters_def:
form_validators = parameters_def["validators"]
source_validators = []
# convert the current tool form structure to the persisted YAML-definition style
for form_validator in form_validators:
source_validators.append(
Expand All @@ -1678,7 +1736,7 @@ def _parse_state_into_dict(self):
"type": "regex",
}
)
rval["validators"] = source_validators
rval["validators"] = source_validators
restrictions_cond_values = parameters_def.get("restrictions")
if restrictions_cond_values:

Expand Down

0 comments on commit 1f1c118

Please sign in to comment.