From 199fe7e0fccbeeb55501bad54f8ae4e44e7c100c Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sun, 7 Oct 2018 18:33:15 +0200 Subject: [PATCH 01/20] Allow saving workflows with input parameters --- lib/galaxy/managers/workflows.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 319b01dc872..30d2a12cffb 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -846,6 +846,8 @@ class WorkflowContentsManager(UsesAnnotations): label = "Input Dataset" elif step_type == "data_collection_input": label = "Input Dataset Collection" + elif step_type == 'parameter_input': + label = "Input Parameter" else: raise ValueError("Invalid step_type %s" % step_type) if legacy: From 638b1b7581d38b9044e17937f308c82fd36663ee Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sun, 7 Oct 2018 18:33:46 +0200 Subject: [PATCH 02/20] Allow inserting input parameter into workflow --- lib/galaxy/workflow/modules.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 41e1fbba21c..ec2a78867e6 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -1300,6 +1300,11 @@ def load_module_sections(trans): "title": "Input Dataset Collection", "description": "Input dataset collection" }, + { + "name": "parameter_input", + "title": "Parameter Input", + "description": "Simple inputs used for workflow logic" + }, ], } @@ -1312,12 +1317,7 @@ def load_module_sections(trans): "name": "pause", "title": "Pause Workflow for Dataset Review", "description": "Pause for Review" - }, - { - "name": "parameter_input", - "title": "Parameter Input", - "description": "Simple inputs used for workflow logic" - }, + } ], } From c09d1bc150775909f0e2a8f1c48ad0b2b70afea0 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 8 Oct 2018 11:49:24 +0200 Subject: [PATCH 03/20] WIP: add StepParameter inputs to workflow editor In the current state when a text, integer, float, boolean or boolean parameter is set as "Set at Runtime" a new connectable input appears for the tool node, to which a Parameter can be properly dragged and connected. This still needs a little more separation from data inputs/outputs, and the new connection isn't persisted in a meaningful way. --- .../scripts/mvc/workflow/workflow-node.js | 17 ++++++ .../mvc/workflow/workflow-terminals.js | 17 ++++++ .../mvc/workflow/workflow-view-data.js | 58 ++++++++++++++++++- .../mvc/workflow/workflow-view-node.js | 52 +++++++++++++++++ .../mvc/workflow/workflow-view-terminals.js | 27 +++++++++ lib/galaxy/managers/workflows.py | 1 + lib/galaxy/workflow/modules.py | 6 ++ 7 files changed, 177 insertions(+), 1 deletion(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index d4c07360b1b..805c2051699 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -6,6 +6,14 @@ import NodeView from "mvc/workflow/workflow-view-node"; /* global $ */ /* global Galaxy */ +var StepParameterTypes = [ + 'text', + 'integer', + 'float', + 'boolean', + 'color', +] + var Node = Backbone.Model.extend({ initialize: function(app, attr) { this.app = app; @@ -217,12 +225,21 @@ var Node = Backbone.Model.extend({ $.each(data.data_inputs, (i, input) => { nodeView.addDataInput(input); }); + $.each(node.config_form.inputs, (i, input) => { + if (input.value.__class__ == 'RuntimeValue' && StepParameterTypes.includes(input.type)){ + console.log('Adding Parameter Input'); + nodeView.addParameterInput(input); + } + }); if (data.data_inputs.length > 0 && data.data_outputs.length > 0) { nodeView.addRule(); } $.each(data.data_outputs, (i, output) => { nodeView.addDataOutput(output); }); + $.each(data.input_parameters, (i, input_parameter) => { + nodeView.addParameterOutput(input_parameter); + }); nodeView.render(); this.app.workflow.node_changed(this, true); }, diff --git a/client/galaxy/scripts/mvc/workflow/workflow-terminals.js b/client/galaxy/scripts/mvc/workflow/workflow-terminals.js index dcea5622c1f..dbbae41ad1d 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-terminals.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-terminals.js @@ -431,6 +431,18 @@ var InputTerminal = BaseInputTerminal.extend({ } }); +var InputParameterTerminal = BaseInputTerminal.extend({ + update: function(input) { + this.type = input.type; + }, + connect: function(connector) { + BaseInputTerminal.prototype.connect.call(this, connector); + }, + attachable: function(other) { + return this.type == other.attributes.type; + }, +}); + var InputCollectionTerminal = BaseInputTerminal.extend({ update: function(input) { this.multiple = false; @@ -566,9 +578,14 @@ var OutputCollectionTerminal = Terminal.extend({ } }); +var OutputParameterTerminal = Terminal.extend({ +}); + export default { InputTerminal: InputTerminal, + InputParameterTerminal: InputParameterTerminal, OutputTerminal: OutputTerminal, + OutputParameterTerminal: OutputParameterTerminal, InputCollectionTerminal: InputCollectionTerminal, OutputCollectionTerminal: OutputCollectionTerminal, TerminalMapping: TerminalMapping, diff --git a/client/galaxy/scripts/mvc/workflow/workflow-view-data.js b/client/galaxy/scripts/mvc/workflow/workflow-view-data.js index 4f36991a83b..d7ff3b0b708 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-view-data.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-view-data.js @@ -90,6 +90,61 @@ var DataOutputView = Backbone.View.extend({ } }); +var ParameterOutputView = Backbone.View.extend({ + className: "form-row dataRow", + + initialize: function(options) { + this.output = options.output; + this.terminalElement = options.terminalElement; + this.nodeView = options.nodeView; + + var output = this.output; + var label = output.label || output.name; + var node = this.nodeView.node; + + this.$el.html(label); + this.calloutView = null; + if (["tool", "subworkflow"].indexOf(node.type) >= 0) { + var calloutView = new OutputCalloutView({ + label: label, + output: output, + node: node + }); + this.calloutView = calloutView; + this.$el.append(calloutView.el); + this.$el.hover( + () => { + calloutView.hoverImage(); + }, + () => { + calloutView.resetImage(); + } + ); + } + this.$el.css({ + position: "absolute", + left: -1000, + top: -1000, + display: "none" + }); + $("body").append(this.el); + this.nodeView.updateMaxWidth(this.$el.outerWidth() + 17); + this.$el + .css({ + position: "", + left: "", + top: "", + display: "" + }) + .detach(); + }, + redrawWorkflowOutput: function() { + if (this.calloutView) { + this.calloutView.resetImage(); + } + } +}); + var OutputCalloutView = Backbone.View.extend({ tagName: "div", @@ -153,5 +208,6 @@ var OutputCalloutView = Backbone.View.extend({ export default { DataInputView: DataInputView, - DataOutputView: DataOutputView + DataOutputView: DataOutputView, + ParameterOutputView: ParameterOutputView, }; diff --git a/client/galaxy/scripts/mvc/workflow/workflow-view-node.js b/client/galaxy/scripts/mvc/workflow/workflow-view-node.js index 82defeb87fe..685df5df335 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-view-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-view-node.js @@ -79,6 +79,43 @@ export default Backbone.View.extend({ return terminalView; }, + addParameterInput: function(input, body) { + var skipResize = true; + if (!body) { + body = this.$(".inputs"); + // initial addition to node - resize input to help calculate node + // width. + skipResize = false; + } + var terminalView = this.terminalViews[input.name]; + var terminalViewClass = TerminalViews.InputParameterTerminalView; + if (terminalView && !(terminalView instanceof terminalViewClass)) { + terminalView.el.terminal.destroy(); + terminalView = null; + } + if (!terminalView) { + terminalView = new terminalViewClass({ + node: this.node, + input: input + }); + } else { + var terminal = terminalView.el.terminal; + terminal.update(input); + terminal.destroyInvalidConnections(); + } + this.terminalViews[input.name] = terminalView; + var terminalElement = terminalView.el; + var inputView = new DataViews.DataInputView({ + terminalElement: terminalElement, + input: input, + nodeView: this, + skipResize: skipResize + }); + var ib = inputView.$el; + body.append(ib.prepend(terminalView.terminalElements())); + return terminalView; + }, + addDataOutput: function(output) { var terminalViewClass = output.collection ? TerminalViews.OutputCollectionTerminalView @@ -96,6 +133,21 @@ export default Backbone.View.extend({ this.tool_body.append(outputView.$el.append(terminalView.terminalElements())); }, + addParameterOutput: function(input_parameter) { + var terminalViewClass = TerminalViews.OutputParameterTerminalView; + var terminalView = new terminalViewClass({ + node: this.node, + output: input_parameter, + }); + var parameterView = new DataViews.ParameterOutputView({ + output: input_parameter, + terminalElement: terminalView.el, + nodeView: this + }); + this.outputViews[input_parameter.name] = parameterView; + this.tool_body.append(parameterView.$el.append(terminalView.terminalElements())); + }, + redrawWorkflowOutputs: function() { _.each(this.outputViews, outputView => { outputView.redrawWorkflowOutput(); diff --git a/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js b/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js index 045aa3f369a..7ec3a7d4f3b 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js @@ -161,6 +161,17 @@ var InputTerminalView = BaseInputTerminalView.extend({ } }); +var InputParameterTerminalView = BaseInputTerminalView.extend({ + terminalMappingClass: Terminals.TerminalMapping, + terminalMappingViewClass: InputTerminalMappingView, + terminalForInput: function(input) { + return new Terminals.InputParameterTerminal({ + element: this.el, + input: input + }); + } +}); + var InputCollectionTerminalView = BaseInputTerminalView.extend({ terminalMappingClass: Terminals.TerminalMapping, terminalMappingViewClass: InputTerminalMappingView, @@ -278,9 +289,25 @@ var OutputCollectionTerminalView = BaseOutputTerminalView.extend({ } }); +var OutputParameterTerminalView = BaseOutputTerminalView.extend({ + terminalMappingClass: Terminals.TerminalMapping, + terminalMappingViewClass: TerminalMappingView, + terminalForOutput: function(output) { + var collection_type = output.collection_type; + var collection_type_source = output.collection_type_source; + var terminal = new Terminals.OutputCollectionTerminal({ + element: this.el, + type: output.type, + }); + return terminal; + } +}); + export default { InputTerminalView: InputTerminalView, + InputParameterTerminalView: InputParameterTerminalView, OutputTerminalView: OutputTerminalView, + OutputParameterTerminalView: OutputParameterTerminalView, InputCollectionTerminalView: InputCollectionTerminalView, OutputCollectionTerminalView: OutputCollectionTerminalView }; diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 30d2a12cffb..8454162f07b 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -573,6 +573,7 @@ class WorkflowContentsManager(UsesAnnotations): 'errors': module.get_errors(), 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), + 'input_parameters': module.get_input_parameters(), 'config_form': config_form, 'annotation': annotation_str, 'post_job_actions': {}, diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index ec2a78867e6..3ca7b2f28ca 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -158,6 +158,9 @@ class WorkflowModule(object): def get_data_outputs(self): return [] + def get_input_parameters(self): + return [] + def get_post_job_actions(self, incoming): return [] @@ -638,6 +641,9 @@ class InputParameterModule(WorkflowModule): def get_data_inputs(self): return [] + def get_input_parameters(self): + return [dict(name=self.name, label=self.label, type=self.parameter_type)] + def execute(self, trans, progress, invocation_step, use_cached_job=False): step = invocation_step.workflow_step step_outputs = dict(output=step.state.inputs['input']) From 99c270fd77367a813af6202465d5453be78ac4eb Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 8 Oct 2018 13:38:58 +0200 Subject: [PATCH 04/20] Fix rebuilding parameter node --- .../scripts/mvc/workflow/workflow-node.js | 27 ++++++++++++------- lib/galaxy/webapps/galaxy/api/workflows.py | 1 + lib/galaxy/workflow/modules.py | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index 805c2051699..ec2a7ea803c 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -200,6 +200,19 @@ var Node = Backbone.Model.extend({ this.content_id = this.config_form.id; } }, + add_output_parameters: function(input_parameters) { + $.each(input_parameters, (i, input_parameter) => { + this.nodeView.addParameterOutput(input_parameter); + }); + }, + add_input_parameters: function() { + $.each(this.config_form.inputs, (i, input) => { + if (input.value.__class__ == 'RuntimeValue' && StepParameterTypes.includes(input.type)){ + this.nodeView.addParameterInput(input); + } + }); + }, + init_field_data: function(data) { //console.debug("init_field_data: ", data); if (data.type) { @@ -225,21 +238,15 @@ var Node = Backbone.Model.extend({ $.each(data.data_inputs, (i, input) => { nodeView.addDataInput(input); }); - $.each(node.config_form.inputs, (i, input) => { - if (input.value.__class__ == 'RuntimeValue' && StepParameterTypes.includes(input.type)){ - console.log('Adding Parameter Input'); - nodeView.addParameterInput(input); - } - }); + if (data.data_inputs.length > 0 && data.data_outputs.length > 0) { nodeView.addRule(); } $.each(data.data_outputs, (i, output) => { nodeView.addDataOutput(output); }); - $.each(data.input_parameters, (i, input_parameter) => { - nodeView.addParameterOutput(input_parameter); - }); + this.add_input_parameters(); + this.add_output_parameters(data.input_parameters); nodeView.render(); this.app.workflow.node_changed(this, true); }, @@ -330,6 +337,8 @@ var Node = Backbone.Model.extend({ // Won't be present in response for data inputs this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : []; } + this.add_input_parameters(); + this.add_output_parameters(data.input_parameters); // If active, reactivate with new config_form this.markChanged(); this.redraw(); diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 17ccb03773d..09938cc5e22 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -558,6 +558,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn 'name' : module.get_name(), 'tool_state' : module.get_state(), 'data_inputs' : module.get_data_inputs(), + 'input_parameters' : module.get_input_parameters(), 'data_outputs' : module.get_data_outputs(), 'config_form' : module.get_config_form(), 'post_job_actions' : module.get_post_job_actions(inputs) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 3ca7b2f28ca..d90e9ddf158 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -639,7 +639,7 @@ class InputParameterModule(WorkflowModule): return state def get_data_inputs(self): - return [] + return [self.state.inputs] def get_input_parameters(self): return [dict(name=self.name, label=self.label, type=self.parameter_type)] From b0d64a8f09e6dc3d29c069edcb2abe77ee9c3662 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 8 Oct 2018 18:04:43 +0200 Subject: [PATCH 05/20] Fix selecting the correct item from select list select lists don't take a `value` argument. --- lib/galaxy/workflow/modules.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index d90e9ddf158..050791d8858 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -607,14 +607,20 @@ class InputParameterModule(WorkflowModule): optional = self.state.inputs.get("optional", self.default_optional) input_parameter_type = SelectToolParameter(None, XML( ''' - + - ''' % parameter_type)) + ''')) + for i, option in enumerate(input_parameter_type.static_options): + option = list(option) + if option[1] == parameter_type: + # item 0 is option description, item 1 is value, item 2 is "selected" + option[2] = True + input_parameter_type.static_options[i] = tuple(option) return odict([("parameter_type", input_parameter_type), ("optional", BooleanToolParameter(None, Element("param", name="optional", label="Optional", type="boolean", value=optional)))]) From c624cc4bce8b7b2bea9eaf61ecbf8bea485c4640 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 8 Oct 2018 18:15:30 +0200 Subject: [PATCH 06/20] Fix getting correct type --- lib/galaxy/workflow/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 050791d8858..c046a182319 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -648,7 +648,7 @@ class InputParameterModule(WorkflowModule): return [self.state.inputs] def get_input_parameters(self): - return [dict(name=self.name, label=self.label, type=self.parameter_type)] + return [dict(name=self.name, label=self.label, type=self.state.inputs['parameter_type'], optional=self.state.inputs['optional'])] def execute(self, trans, progress, invocation_step, use_cached_job=False): step = invocation_step.workflow_step From 83a25c90e66b52890fcd8ad9288059d87b4f4932 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 10 Oct 2018 11:47:32 +0200 Subject: [PATCH 07/20] Fix restoring saved non-data connections in workflow editor --- lib/galaxy/managers/workflows.py | 2 +- lib/galaxy/model/__init__.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 8454162f07b..ed10938e325 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -601,7 +601,7 @@ class WorkflowContentsManager(UsesAnnotations): visit_input_values(module.tool.inputs, module.state.inputs, callback) # Filter # FIXME: this removes connection without displaying a message currently! - input_connections = [conn for conn in input_connections if conn.input_name in data_input_names] + input_connections = [conn for conn in input_connections if conn.input_name in data_input_names or conn.non_data_connection] # post_job_actions pja_dict = {} for pja in step.post_job_actions: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 2f1ac406e54..42a55deb806 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -4179,14 +4179,10 @@ class WorkflowStepConnection(object): self.input_step_id = None self.input_name = None - def set_non_data_connection(self): - self.output_name = WorkflowStepConnection.NON_DATA_CONNECTION - self.input_name = WorkflowStepConnection.NON_DATA_CONNECTION - @property def non_data_connection(self): - return (self.output_name == WorkflowStepConnection.NON_DATA_CONNECTION and - self.input_name == WorkflowStepConnection.NON_DATA_CONNECTION) + return (self.output_name == self.input_name == WorkflowStepConnection.NON_DATA_CONNECTION or + self.output_step and self.output_step.type == 'parameter_input') def copy(self): # TODO: handle subworkflow ids... From 4e3004bb250f86b8c16a1c94c8599f497488e732 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 10 Oct 2018 13:33:15 +0200 Subject: [PATCH 08/20] Fix node.addOuputParameter --- client/galaxy/scripts/mvc/workflow/workflow-node.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index ec2a7ea803c..121722fdbc5 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -201,8 +201,9 @@ var Node = Backbone.Model.extend({ } }, add_output_parameters: function(input_parameters) { + var self = this; $.each(input_parameters, (i, input_parameter) => { - this.nodeView.addParameterOutput(input_parameter); + self.nodeView.addParameterOutput(input_parameter); }); }, add_input_parameters: function() { From e135fac159e80549ce611a40f4c66598f51632d2 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 10 Oct 2018 16:11:46 +0200 Subject: [PATCH 09/20] Don't render data inputs on parameter_input node --- client/galaxy/scripts/mvc/workflow/workflow-node.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index 121722fdbc5..39f472c74c8 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -237,7 +237,9 @@ var Node = Backbone.Model.extend({ }); node.nodeView = nodeView; $.each(data.data_inputs, (i, input) => { - nodeView.addDataInput(input); + if (nodeView.node.type != 'parameter_input') { + nodeView.addDataInput(input); + } }); if (data.data_inputs.length > 0 && data.data_outputs.length > 0) { @@ -317,8 +319,10 @@ var Node = Backbone.Model.extend({ var new_body = nodeView.newInputsDiv(); var newTerminalViews = {}; _.each(data.data_inputs, input => { - var terminalView = node.nodeView.addDataInput(input, new_body); - newTerminalViews[input.name] = terminalView; + if (nodeView.node.type != 'parameter_input') { + var terminalView = node.nodeView.addDataInput(input, new_body); + newTerminalViews[input.name] = terminalView; + } }); // Cleanup any leftover terminals _.each(_.difference(_.values(nodeView.terminalViews), _.values(newTerminalViews)), unusedView => { From 0340dcb18a99d9d7984eb7566b3826290fe90025 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 10 Oct 2018 17:18:33 +0200 Subject: [PATCH 10/20] More defensive runtime check --- client/galaxy/scripts/mvc/workflow/workflow-node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index 39f472c74c8..5647d5d995a 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -208,7 +208,7 @@ var Node = Backbone.Model.extend({ }, add_input_parameters: function() { $.each(this.config_form.inputs, (i, input) => { - if (input.value.__class__ == 'RuntimeValue' && StepParameterTypes.includes(input.type)){ + if (input.value && input.value.__class__ == 'RuntimeValue' && StepParameterTypes.includes(input.type)){ this.nodeView.addParameterInput(input); } }); From 5a5b4abb5bd4b1b142601a94b4498556c4f14946 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 10 Oct 2018 17:19:00 +0200 Subject: [PATCH 11/20] Allow loading in test-data workfows --- lib/galaxy/workflow/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index c046a182319..c6e0cd48dca 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -648,7 +648,7 @@ class InputParameterModule(WorkflowModule): return [self.state.inputs] def get_input_parameters(self): - return [dict(name=self.name, label=self.label, type=self.state.inputs['parameter_type'], optional=self.state.inputs['optional'])] + return [dict(name=self.name, label=self.label, type=self.state.inputs.get('parameter_type', self.parameter_type), optional=self.state.inputs.get('optional', self.optional))] def execute(self, trans, progress, invocation_step, use_cached_job=False): step = invocation_step.workflow_step From 4812586156b4cf01fe8ee1cb743306cc91ba2c84 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 11 Oct 2018 12:41:26 -0400 Subject: [PATCH 12/20] Add label to input parameters without labels. --- client/galaxy/scripts/mvc/workflow/workflow-view-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-view-data.js b/client/galaxy/scripts/mvc/workflow/workflow-view-data.js index d7ff3b0b708..305e57b478d 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-view-data.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-view-data.js @@ -9,7 +9,7 @@ var DataInputView = Backbone.View.extend({ this.nodeView = options.nodeView; this.terminalElement = options.terminalElement; - this.$el.attr("name", this.input.name).html(this.input.label); + this.$el.attr("name", this.input.name).html(this.input.label || this.input.name); if (!options.skipResize) { this.$el.css({ From dbe4033abe74035082b510c862c7729212367341 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 12 Oct 2018 09:21:11 -0400 Subject: [PATCH 13/20] Give parameter inputs a workflow icon. --- client/galaxy/scripts/mvc/workflow/workflow-icons.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-icons.js b/client/galaxy/scripts/mvc/workflow/workflow-icons.js index a0239205fa2..a3b84c4e543 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-icons.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-icons.js @@ -3,5 +3,6 @@ export default { data_input: "fa-file-o", data_collection_input: "fa-folder-o", subworkflow: "fa-sitemap fa-rotate-270", + parameter_input: "fa-pencil", pause: "fa-pause" }; From 00b8776db521cab884a107f8b66441690f644f88 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 12 Oct 2018 10:27:46 -0400 Subject: [PATCH 14/20] Convert raw parameters from map into formal parameters when invoking wfs. --- lib/galaxy/workflow/run_request.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 26320d03ae1..ff495019438 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -265,6 +265,17 @@ def build_workflow_run_configs(trans, workflow, payload): # inputs with referential integrity if parameters are already normalized (coming from tool form). normalized_inputs = {} + if param_map: + # disentangle raw parameter dictionaries into formal request structures if we can + # to setup proper WorkflowRequestToInputDatasetAssociation, WorkflowRequestToInputDatasetCollectionAssociation + # and WorkflowRequestInputStepParameter objects. + for step in workflow.steps: + normalized_key = step.id + if step.type == "parameter_input": + if normalized_key in param_map: + value = param_map.pop(normalized_key) + normalized_inputs[normalized_key] = value["input"] + steps_by_id = workflow.steps_by_id # Set workflow inputs. for key, input_dict in normalized_inputs.items(): From d74b959419a667b2ff3192c9fde07a2cddc5e90d Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 12 Oct 2018 11:37:09 -0400 Subject: [PATCH 15/20] Revert non-data connection handling, send connections unfiltered to editor. --- lib/galaxy/managers/workflows.py | 2 -- lib/galaxy/model/__init__.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index ed10938e325..209a0b655a2 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -790,8 +790,6 @@ class WorkflowContentsManager(UsesAnnotations): # If the tool is installed we attempt to verify input values # and connections, otherwise the last known state will be dumped without modifications. visit_input_values(module.tool.inputs, module.state.inputs, callback) - # FIXME: this removes connection without displaying a message currently! - input_connections = [conn for conn in input_connections if (conn.input_name in data_input_names or conn.non_data_connection)] # Encode input connections as dictionary input_conn_dict = {} diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 42a55deb806..88659a4d3ad 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -4181,8 +4181,7 @@ class WorkflowStepConnection(object): @property def non_data_connection(self): - return (self.output_name == self.input_name == WorkflowStepConnection.NON_DATA_CONNECTION or - self.output_step and self.output_step.type == 'parameter_input') + return (self.output_name == self.input_name == WorkflowStepConnection.NON_DATA_CONNECTION) def copy(self): # TODO: handle subworkflow ids... From 85d777dbe704ccdbf71898853e542c1c2df05303 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Oct 2018 19:08:18 -0400 Subject: [PATCH 16/20] Separate RuntimeValue into RuntimeValue and ConnectedValue. Use ConnectedValue values to determine what non-data parameters to display connections for. --- client/galaxy/scripts/mvc/form/form-data.js | 4 +- client/galaxy/scripts/mvc/form/form-input.js | 65 +++++++++++++++---- .../galaxy/scripts/mvc/form/form-section.js | 1 + .../scripts/mvc/workflow/workflow-forms.js | 2 + .../scripts/mvc/workflow/workflow-node.js | 2 +- lib/galaxy/managers/workflows.py | 2 +- lib/galaxy/tools/parameters/__init__.py | 9 +-- lib/galaxy/tools/parameters/basic.py | 29 +++++++-- lib/galaxy/workflow/modules.py | 40 ++++++------ 9 files changed, 110 insertions(+), 44 deletions(-) diff --git a/client/galaxy/scripts/mvc/form/form-data.js b/client/galaxy/scripts/mvc/form/form-data.js index 7ea27dda0ff..b55e5cc564e 100644 --- a/client/galaxy/scripts/mvc/form/form-data.js +++ b/client/galaxy/scripts/mvc/form/form-data.js @@ -84,7 +84,9 @@ export var Manager = Backbone.Model.extend({ if (field && field.value) { value = field.value(); if (input.ignore === undefined || input.ignore != value) { - if (field.collapsed && input.collapsible_value) { + if (field.collapsed && field.connected) { + value = { __class__: "ConnectedValue" }; + } else if (field.collapsed && input.collapsible_value) { value = input.collapsible_value; } add(flat_id, input.id, value); diff --git a/client/galaxy/scripts/mvc/form/form-input.js b/client/galaxy/scripts/mvc/form/form-input.js index 56c606a20dd..6ebac7587ef 100644 --- a/client/galaxy/scripts/mvc/form/form-input.js +++ b/client/galaxy/scripts/mvc/form/form-input.js @@ -25,6 +25,7 @@ export default Backbone.View.extend({ this.$collapsible = this.$(".ui-form-collapsible"); this.$collapsible_text = this.$(".ui-form-collapsible-text"); this.$collapsible_icon = this.$(".ui-form-collapsible-icon"); + this.$connected_icon = this.$(".ui-form-connected-icon"); this.$title = this.$(".ui-form-title"); this.$title_text = this.$(".ui-form-title-text"); this.$error_text = this.$(".ui-form-error-text"); @@ -36,16 +37,35 @@ export default Backbone.View.extend({ // decide wether to expand or collapse fields var collapsible_value = this.model.get("collapsible_value"); + var value = JSON.stringify(this.model.get("value")); + var connected = value == JSON.stringify({ __class__: "ConnectedValue" }); + this.field.connected = connected; this.field.collapsed = - collapsible_value !== undefined && - JSON.stringify(this.model.get("value")) == JSON.stringify(collapsible_value); + this.field.connected || + (collapsible_value !== undefined && + JSON.stringify(this.model.get("value")) == JSON.stringify(collapsible_value)); this.listenTo(this.model, "change", this.render, this); this.render(); // add click handler var self = this; - this.$collapsible.on("click", () => { + this.$collapsible_icon.on("click", () => { + if (self.field.connected) { + return; + } self.field.collapsed = !self.field.collapsed; + if (self.field.collapsed) { + self.field.connected = false; + } + app.trigger && app.trigger("change"); + self.render(); + }); + this.$connected_icon.on("click", () => { + self.field.connected = !self.field.connected; + self.field.collapsed = self.field.connected; + if (!self.field.connected) { + this.model.set("value", null); + } app.trigger && app.trigger("change"); self.render(); }); @@ -108,21 +128,41 @@ export default Backbone.View.extend({ style: this.model.get("style") }); // render collapsible options - if ( + const connected = this.field.connected; + const collapsible = + !connected && !this.field.collapsible_disabled && !this.model.get("disabled") && - this.model.get("collapsible_value") !== undefined - ) { + this.model.get("collapsible_value") !== undefined; + const connectable = this.model.get("connectable"); + if (collapsible || connectable) { var collapsible_state = this.field.collapsed ? "enable" : "disable"; this.$title_text.hide(); this.$collapsible.show(); this.$collapsible_text.text(this.model.get("label")); - this.$collapsible_icon - .removeClass() - .addClass("icon") - .addClass(this.model.get(`cls_${collapsible_state}`)) - .attr("data-original-title", this.model.get(`text_${collapsible_state}`)) - .tooltip({ placement: "bottom" }); + if (collapsible && !connected) { + this.$collapsible_icon + .removeClass() + .addClass("icon ui-form-collapsible-icon") + .addClass(this.model.get(`cls_${collapsible_state}`)) + .attr("data-original-title", this.model.get(`text_${collapsible_state}`)) + .tooltip({ placement: "bottom" }) + .show(); + } else { + this.$collapsible_icon.hide(); + } + if (connectable) { + const connectedIconStyle = this.field.connected ? "fa fa-times" : "fa fa-arrows-h"; + this.$connected_icon + .removeClass() + .addClass("icon ui-form-connected-icon") + .addClass(connectedIconStyle) + .attr("data-original-title", this.model.get(`text_${collapsible_state}`)) + .tooltip({ placement: "bottom" }) + .show(); + } else { + this.$connected_icon.hide(); + } } else { this.$title_text.show().text(this.model.get("label")); this.$collapsible.hide(); @@ -145,6 +185,7 @@ export default Backbone.View.extend({ $("
") .addClass("ui-form-collapsible") .append($("").addClass("ui-form-collapsible-icon")) + .append($("").addClass("ui-form-connected-icon")) .append($("").addClass("ui-form-collapsible-text")) ) .append($("").addClass("ui-form-title-text")) diff --git a/client/galaxy/scripts/mvc/form/form-section.js b/client/galaxy/scripts/mvc/form/form-section.js index 2b0dbbe6f73..75111933817 100644 --- a/client/galaxy/scripts/mvc/form/form-section.js +++ b/client/galaxy/scripts/mvc/form/form-section.js @@ -175,6 +175,7 @@ var View = Backbone.View.extend({ text_value: input_def.text_value, collapsible_value: input_def.collapsible_value, collapsible_preview: input_def.collapsible_preview, + connectable: input_def.connectable, help: input_def.help, argument: input_def.argument, disabled: input_def.disabled, diff --git a/client/galaxy/scripts/mvc/workflow/workflow-forms.js b/client/galaxy/scripts/mvc/workflow/workflow-forms.js index ba95ef2506a..d9602b540f3 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-forms.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-forms.js @@ -90,6 +90,7 @@ var Tool = Backbone.View.extend({ var options = form.model.attributes; Utils.deepeach(options.inputs, input => { if (input.type) { + input.connectable = true; if (["data", "data_collection"].indexOf(input.type) != -1) { input.type = "hidden"; input.info = `Data input '${input.name}' (${Utils.textify(input.extensions)})`; @@ -105,6 +106,7 @@ var Tool = Backbone.View.extend({ }); Utils.deepeach(options.inputs, input => { if (input.type === "conditional") { + input.connectable = false; input.test_param.collapsible_value = undefined; } }); diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index 5647d5d995a..f8ad52645f1 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -208,7 +208,7 @@ var Node = Backbone.Model.extend({ }, add_input_parameters: function() { $.each(this.config_form.inputs, (i, input) => { - if (input.value && input.value.__class__ == 'RuntimeValue' && StepParameterTypes.includes(input.type)){ + if (input.value && input.value.__class__ == 'ConnectedValue' && StepParameterTypes.includes(input.type)){ this.nodeView.addParameterInput(input); } }); diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 209a0b655a2..661a508a322 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -561,7 +561,7 @@ class WorkflowContentsManager(UsesAnnotations): data['upgrade_messages'][step.order_index] = {module.tool.name: "\n".join(module.version_changes)} # Get user annotation. annotation_str = self.get_item_annotation_str(trans.sa_session, trans.user, step) or '' - config_form = module.get_config_form() + config_form = module.get_config_form(step=step) # Pack attributes into plain dictionary step_dict = { 'id': step.order_index, diff --git a/lib/galaxy/tools/parameters/__init__.py b/lib/galaxy/tools/parameters/__init__.py index b9f108ee965..006b7c783fb 100644 --- a/lib/galaxy/tools/parameters/__init__.py +++ b/lib/galaxy/tools/parameters/__init__.py @@ -9,7 +9,7 @@ from boltons.iterutils import remap from galaxy.util.expressions import ExpressionContext from galaxy.util.json import safe_loads -from .basic import DataCollectionToolParameter, DataToolParameter, RuntimeValue, SelectToolParameter +from .basic import DataCollectionToolParameter, DataToolParameter, is_runtime_value, runtime_to_json, SelectToolParameter from .grouping import Conditional, Repeat, Section, UploadDataset REPLACE_ON_TRUTHY = object() @@ -179,11 +179,8 @@ def check_param(trans, param, incoming_value, param_values): error = None try: if trans.workflow_building_mode: - if isinstance(value, RuntimeValue): - return [{'__class__' : 'RuntimeValue'}, None] - if isinstance(value, dict): - if value.get('__class__') == 'RuntimeValue': - return [value, None] + if is_runtime_value(value): + return [runtime_to_json(value), None] value = param.from_json(value, trans, param_values) param.validate(value, trans) except ValueError as e: diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index d2cae015407..cb3c5ae00b0 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -56,7 +56,7 @@ def contains_workflow_parameter(value, search=False): def is_runtime_value(value): return isinstance(value, RuntimeValue) or (isinstance(value, dict) - and value.get("__class__") == "RuntimeValue") + and value.get("__class__") in ["RuntimeValue", "ConnectedValue"]) def is_runtime_context(trans, other_values): @@ -155,13 +155,13 @@ class ToolParameter(Dictifiable): def value_to_basic(self, value, app, use_security=False): if is_runtime_value(value): - return {'__class__': 'RuntimeValue'} + return runtime_to_json(value) return self.to_json(value, app, use_security) def value_from_basic(self, value, app, ignore_errors=False): # Handle Runtime and Unvalidated values if is_runtime_value(value): - return RuntimeValue() + return runtime_to_object(value) elif isinstance(value, dict) and value.get('__class__') == 'UnvalidatedValue': return value['value'] # Delegate to the 'to_python' method @@ -2246,7 +2246,7 @@ class BaseJsonToolParameter(ToolParameter): def value_to_basic(self, value, app, use_security=False): if is_runtime_value(value): - return {'__class__': 'RuntimeValue'} + return runtime_to_json(value) return value def to_json(self, value, app, use_security): @@ -2323,8 +2323,29 @@ parameter_types = dict( ) +def runtime_to_json(runtime_value): + if isinstance(runtime_value, ConnectedValue) or (isinstance(runtime_value, dict) and runtime_value["__class__"] == "ConnectedValue"): + return {"__class__": "ConnectedValue"} + else: + return {"__class__": "RuntimeValue"} + + +def runtime_to_object(runtime_value): + if isinstance(runtime_value, ConnectedValue) or (isinstance(runtime_value, dict) and runtime_value["__class__"] == "ConnectedValue"): + return ConnectedValue() + else: + return RuntimeValue() + + class RuntimeValue(object): """ Wrapper to note a value that is not yet set, but will be required at runtime. """ pass + + +class ConnectedValue(RuntimeValue): + """ + Wrapper to note a value that is not yet set, but will be inferred from a connection. + """ + pass diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index c6e0cd48dca..e5b7f91b26b 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -30,6 +30,7 @@ from galaxy.tools.parameters import ( ) from galaxy.tools.parameters.basic import ( BooleanToolParameter, + ConnectedValue, DataCollectionToolParameter, DataToolParameter, is_runtime_value, @@ -175,7 +176,7 @@ class WorkflowModule(object): """ Replace connected inputs with placeholder/dummy values. """ pass - def get_config_form(self): + def get_config_form(self, step=None): """ Serializes input parameters of a module into input dictionaries. """ return { 'title' : self.name, @@ -902,9 +903,9 @@ class ToolModule(WorkflowModule): ) return data_outputs - def get_config_form(self): + def get_config_form(self, step=None): if self.tool: - self.add_dummy_datasets() + self.add_dummy_datasets(connections=step and step.input_connections) incoming = {} params_to_incoming(incoming, self.tool.inputs, self.state.inputs, self.trans.app) return self.tool.to_json(self.trans, incoming, workflow_building_mode=True) @@ -923,22 +924,23 @@ class ToolModule(WorkflowModule): # Any input needs to have value RuntimeValue or obtain the value from connected steps def callback(input, prefixed_name, context, **kwargs): - if isinstance(input, DataToolParameter) or isinstance(input, DataCollectionToolParameter): - if connections is not None and steps is not None and self.trans.workflow_building_mode is workflow_building_modes.USE_HISTORY: - if prefixed_name in input_connections_by_name: - connection = input_connections_by_name[prefixed_name] - output_step = next(output_step for output_step in steps if connection.output_step_id == output_step.id) - if output_step.type.startswith('data'): - output_inputs = output_step.module.get_runtime_inputs(connections=connections) - output_value = output_inputs['input'].get_initial_value(self.trans, context) - if isinstance(input, DataToolParameter) and isinstance(output_value, self.trans.app.model.HistoryDatasetCollectionAssociation): - return output_value.to_hda_representative() - return output_value - return RuntimeValue() - else: - return input.get_initial_value(self.trans, context) - elif connections is None or prefixed_name in input_connections_by_name: - return RuntimeValue() + input_type = input.type + is_data = input_type in ['data', 'data_collection'] + if is_data and connections is not None and steps is not None and self.trans.workflow_building_mode is workflow_building_modes.USE_HISTORY: + if prefixed_name in input_connections_by_name: + connection = input_connections_by_name[prefixed_name] + output_step = next(output_step for output_step in steps if connection.output_step_id == output_step.id) + if output_step.type.startswith('data'): + output_inputs = output_step.module.get_runtime_inputs(connections=connections) + output_value = output_inputs['input'].get_initial_value(self.trans, context) + if input_type == "data" and isinstance(output_value, self.trans.app.model.HistoryDatasetCollectionAssociation): + return output_value.to_hda_representative() + return output_value + return ConnectedValue() + else: + return input.get_initial_value(self.trans, context) + elif (is_data and connections is None) or prefixed_name in input_connections_by_name: + return ConnectedValue() visit_input_values(self.tool.inputs, self.state.inputs, callback) else: raise ToolMissingException("Tool %s missing. Cannot add dummy datasets." % self.tool_id) From dd8d74f364770a1272555a3753f5d1e1288315e1 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 24 Oct 2018 11:12:05 -0400 Subject: [PATCH 17/20] Unify data/non-data input/output handling for workflow editor. Don't treat data and non-data inputs/outputs different at the API boundary or at the workflow-manager.js level. Keep differences at the level of walking inputs/outputs in the workflow module on the backend and at the terminal definition (models and views) level on the frontend. Make the code and interfaces more consistent in my opinion and is more compatible with future directions I'd like to take things with mix-mode inputs/outputs for CWL. --- .../scripts/mvc/workflow/workflow-node.js | 46 ++++-------- .../mvc/workflow/workflow-view-node.js | 73 ++++--------------- .../qunit/tests/workflow_editor_tests.js | 16 ++-- lib/galaxy/managers/workflows.py | 9 +-- lib/galaxy/webapps/galaxy/api/workflows.py | 5 +- lib/galaxy/workflow/modules.py | 69 +++++++++++------- lib/galaxy/workflow/run.py | 11 +-- test/api/test_workflows.py | 8 +- 8 files changed, 90 insertions(+), 147 deletions(-) diff --git a/client/galaxy/scripts/mvc/workflow/workflow-node.js b/client/galaxy/scripts/mvc/workflow/workflow-node.js index f8ad52645f1..87b333e5c53 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-node.js @@ -200,19 +200,6 @@ var Node = Backbone.Model.extend({ this.content_id = this.config_form.id; } }, - add_output_parameters: function(input_parameters) { - var self = this; - $.each(input_parameters, (i, input_parameter) => { - self.nodeView.addParameterOutput(input_parameter); - }); - }, - add_input_parameters: function() { - $.each(this.config_form.inputs, (i, input) => { - if (input.value && input.value.__class__ == 'ConnectedValue' && StepParameterTypes.includes(input.type)){ - this.nodeView.addParameterInput(input); - } - }); - }, init_field_data: function(data) { //console.debug("init_field_data: ", data); @@ -236,27 +223,23 @@ var Node = Backbone.Model.extend({ node: node }); node.nodeView = nodeView; - $.each(data.data_inputs, (i, input) => { - if (nodeView.node.type != 'parameter_input') { - nodeView.addDataInput(input); - } + $.each(data.inputs, (i, input) => { + nodeView.addDataInput(input); }); - if (data.data_inputs.length > 0 && data.data_outputs.length > 0) { + if (data.inputs.length > 0 && data.outputs.length > 0) { nodeView.addRule(); } - $.each(data.data_outputs, (i, output) => { + $.each(data.outputs, (i, output) => { nodeView.addDataOutput(output); }); - this.add_input_parameters(); - this.add_output_parameters(data.input_parameters); nodeView.render(); this.app.workflow.node_changed(this, true); }, update_field_data: function(data) { var node = this; var nodeView = node.nodeView; - // remove unused output views and remove pre-existing output views from data.data_outputs, + // remove unused output views and remove pre-existing output views from data.outputs, // so that these are not added twice. var unused_outputs = []; // nodeView.outputViews contains pre-existing outputs, @@ -264,7 +247,7 @@ var Node = Backbone.Model.extend({ // Now we gather the unused outputs $.each(nodeView.outputViews, (i, output_view) => { var cur_name = output_view.output.name; - var data_names = data.data_outputs; + var data_names = data.outputs; var cur_name_in_data_outputs = false; _.each(data_names, data_name => { if (data_name.name == cur_name) { @@ -292,7 +275,7 @@ var Node = Backbone.Model.extend({ node.workflow_outputs.splice(i, 1); // removes output from list of workflow outputs } }); - $.each(data.data_outputs, (i, output) => { + $.each(data.outputs, (i, output) => { if (!nodeView.outputViews[output.name]) { nodeView.addDataOutput(output); // add data output if it does not yet exist } else { @@ -318,11 +301,9 @@ var Node = Backbone.Model.extend({ var old_body = nodeView.$("div.inputs"); var new_body = nodeView.newInputsDiv(); var newTerminalViews = {}; - _.each(data.data_inputs, input => { - if (nodeView.node.type != 'parameter_input') { - var terminalView = node.nodeView.addDataInput(input, new_body); - newTerminalViews[input.name] = terminalView; - } + _.each(data.inputs, input => { + var terminalView = node.nodeView.addDataInput(input, new_body); + newTerminalViews[input.name] = terminalView; }); // Cleanup any leftover terminals _.each(_.difference(_.values(nodeView.terminalViews), _.values(newTerminalViews)), unusedView => { @@ -334,16 +315,15 @@ var Node = Backbone.Model.extend({ // type (not really valid right?) but adding special logic here for // data collection input parameters that can have their collection // change. - if (data.data_outputs.length == 1 && "collection_type" in data.data_outputs[0]) { - nodeView.updateDataOutput(data.data_outputs[0]); + var data_outputs = data.outputs; + if (data_outputs.length == 1 && "collection_type" in data_outputs[0]) { + nodeView.updateDataOutput(data_outputs[0]); } old_body.replaceWith(new_body); if ("workflow_outputs" in data) { // Won't be present in response for data inputs this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : []; } - this.add_input_parameters(); - this.add_output_parameters(data.input_parameters); // If active, reactivate with new config_form this.markChanged(); this.redraw(); diff --git a/client/galaxy/scripts/mvc/workflow/workflow-view-node.js b/client/galaxy/scripts/mvc/workflow/workflow-view-node.js index 685df5df335..f07e7a72ee6 100644 --- a/client/galaxy/scripts/mvc/workflow/workflow-view-node.js +++ b/client/galaxy/scripts/mvc/workflow/workflow-view-node.js @@ -48,47 +48,12 @@ export default Backbone.View.extend({ skipResize = false; } var terminalView = this.terminalViews[input.name]; - var terminalViewClass = - input.input_type == "dataset_collection" - ? TerminalViews.InputCollectionTerminalView - : TerminalViews.InputTerminalView; - if (terminalView && !(terminalView instanceof terminalViewClass)) { - terminalView.el.terminal.destroy(); - terminalView = null; + var terminalViewClass = TerminalViews.InputTerminalView; + if (input.input_type == "dataset_collection") { + terminalViewClass = TerminalViews.InputCollectionTerminalView; + } else if (input.input_type == "parameter") { + terminalViewClass = TerminalViews.InputParameterTerminalView; } - if (!terminalView) { - terminalView = new terminalViewClass({ - node: this.node, - input: input - }); - } else { - var terminal = terminalView.el.terminal; - terminal.update(input); - terminal.destroyInvalidConnections(); - } - this.terminalViews[input.name] = terminalView; - var terminalElement = terminalView.el; - var inputView = new DataViews.DataInputView({ - terminalElement: terminalElement, - input: input, - nodeView: this, - skipResize: skipResize - }); - var ib = inputView.$el; - body.append(ib.prepend(terminalView.terminalElements())); - return terminalView; - }, - - addParameterInput: function(input, body) { - var skipResize = true; - if (!body) { - body = this.$(".inputs"); - // initial addition to node - resize input to help calculate node - // width. - skipResize = false; - } - var terminalView = this.terminalViews[input.name]; - var terminalViewClass = TerminalViews.InputParameterTerminalView; if (terminalView && !(terminalView instanceof terminalViewClass)) { terminalView.el.terminal.destroy(); terminalView = null; @@ -117,14 +82,19 @@ export default Backbone.View.extend({ }, addDataOutput: function(output) { - var terminalViewClass = output.collection - ? TerminalViews.OutputCollectionTerminalView - : TerminalViews.OutputTerminalView; + var terminalViewClass = TerminalViews.OutputTerminalView; + var outputViewClass = DataViews.DataOutputView; + if (output.collection) { + terminalViewClass = TerminalViews.OutputCollectionTerminalView; + } else if (output.parameter) { + terminalViewClass = TerminalViews.OutputParameterTerminalView; + outputViewClass = DataViews.ParameterOutputView; + } var terminalView = new terminalViewClass({ node: this.node, output: output }); - var outputView = new DataViews.DataOutputView({ + var outputView = new outputViewClass({ output: output, terminalElement: terminalView.el, nodeView: this @@ -133,21 +103,6 @@ export default Backbone.View.extend({ this.tool_body.append(outputView.$el.append(terminalView.terminalElements())); }, - addParameterOutput: function(input_parameter) { - var terminalViewClass = TerminalViews.OutputParameterTerminalView; - var terminalView = new terminalViewClass({ - node: this.node, - output: input_parameter, - }); - var parameterView = new DataViews.ParameterOutputView({ - output: input_parameter, - terminalElement: terminalView.el, - nodeView: this - }); - this.outputViews[input_parameter.name] = parameterView; - this.tool_body.append(parameterView.$el.append(terminalView.terminalElements())); - }, - redrawWorkflowOutputs: function() { _.each(this.outputViews, outputView => { outputView.redrawWorkflowOutput(); diff --git a/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js b/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js index 263e78d9148..59f8311935f 100644 --- a/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js +++ b/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js @@ -357,16 +357,16 @@ QUnit.module("Node unit test", { }, init_field_data_simple: function(option_overrides) { var data = Utils.merge(option_overrides, { - data_inputs: [{ name: "input1", extensions: ["data"] }], - data_outputs: [{ name: "output1", extensions: ["data"] }], + inputs: [{ name: "input1", extensions: ["data"] }], + outputs: [{ name: "output1", extensions: ["data"] }], label: null }); this.node.init_field_data(data); }, update_field_data_with_new_input: function(option_overrides) { var new_data = Utils.merge(option_overrides, { - data_inputs: [{ name: "input1", extensions: ["data"] }, { name: "extra_0|input1", extensions: ["data"] }], - data_outputs: [{ name: "output1", extensions: ["data"] }], + inputs: [{ name: "input1", extensions: ["data"] }, { name: "extra_0|input1", extensions: ["data"] }], + outputs: [{ name: "output1", extensions: ["data"] }], post_job_actions: "{}", label: "New Label" }); @@ -401,8 +401,8 @@ QUnit.test("init_field_data properties", function(assert) { var node = this.node; this.expect_workflow_node_changed(assert, function() { var data = { - data_inputs: [], - data_outputs: [], + inputs: [], + outputs: [], type: "tool", name: "cat1", config_form: "{}", @@ -507,8 +507,8 @@ QUnit.test("update_field_data destroys old terminals", function(assert) { var node = this.node; this.expect_workflow_node_changed(assert, function() { var data = { - data_inputs: [{ name: "input1", extensions: ["data"] }, { name: "willDisappear", extensions: ["data"] }], - data_outputs: [{ name: "output1", extensions: ["data"] }] + inputs: [{ name: "input1", extensions: ["data"] }, { name: "willDisappear", extensions: ["data"] }], + outputs: [{ name: "output1", extensions: ["data"] }] }; node.init_field_data(data); var old_input_terminal = node.input_terminals.willDisappear; diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 661a508a322..f03fa92d17f 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -571,9 +571,8 @@ class WorkflowContentsManager(UsesAnnotations): 'name': module.get_name(), 'tool_state': module.get_state(), 'errors': module.get_errors(), - 'data_inputs': module.get_data_inputs(), - 'data_outputs': module.get_data_outputs(), - 'input_parameters': module.get_input_parameters(), + 'inputs': module.get_all_inputs(connectable_only=True), + 'outputs': module.get_all_outputs(), 'config_form': config_form, 'annotation': annotation_str, 'post_job_actions': {}, @@ -654,12 +653,12 @@ class WorkflowContentsManager(UsesAnnotations): """ for order_index in sorted(steps): step = steps[order_index] - for i, step_data_output in enumerate(step['data_outputs']): + for i, step_data_output in enumerate(step['outputs']): if step_data_output.get('collection_type_source') and step_data_output['collection_type'] is None: collection_type_source = step_data_output['collection_type_source'] for input_connection in step['input_connections'].get(collection_type_source, []): input_step = steps[input_connection['id']] - for input_step_data_output in input_step['data_outputs']: + for input_step_data_output in input_step['outputs']: if input_step_data_output['name'] == input_connection['output_name']: step_data_output['collection_type'] = input_step_data_output.get('collection_type') return steps diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 09938cc5e22..481b553f50b 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -557,9 +557,8 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn 'annotation' : inputs.get('__annotation', ''), 'name' : module.get_name(), 'tool_state' : module.get_state(), - 'data_inputs' : module.get_data_inputs(), - 'input_parameters' : module.get_input_parameters(), - 'data_outputs' : module.get_data_outputs(), + 'inputs' : module.get_all_inputs(connectable_only=True), + 'outputs' : module.get_all_outputs(), 'config_form' : module.get_config_form(), 'post_job_actions' : module.get_post_job_actions(inputs) } diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index e5b7f91b26b..9743d4f42c9 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -35,7 +35,7 @@ from galaxy.tools.parameters.basic import ( DataToolParameter, is_runtime_value, parameter_types, - RuntimeValue, + runtime_to_json, SelectToolParameter, TextToolParameter, workflow_building_modes @@ -152,15 +152,18 @@ class WorkflowModule(object): """ This returns inputs displayed in the workflow editor """ return {} + def get_all_inputs(self, data_only=False, connectable_only=False): + return [] + def get_data_inputs(self): """ Get configure time data input descriptions. """ + return self.get_all_inputs(data_only=True) + + def get_all_outputs(self, data_only=False): return [] def get_data_outputs(self): - return [] - - def get_input_parameters(self): - return [] + return self.get_all_outputs(data_only=True) def get_post_job_actions(self, incoming): return [] @@ -365,7 +368,7 @@ class SubWorkflowModule(WorkflowModule): return self.subworkflow.name return self.name - def get_all_inputs(self, data_only=False): + def get_all_inputs(self, data_only=False, connectable_only=False): """ Get configure time data input descriptions. """ # Filter subworkflow steps and get inputs step_to_input_type = { @@ -393,10 +396,7 @@ class SubWorkflowModule(WorkflowModule): inputs.append(input) return inputs - def get_data_inputs(self): - return self.get_all_inputs(data_only=True) - - def get_data_outputs(self): + def get_all_outputs(self, data_only=False): outputs = [] if hasattr(self.subworkflow, 'workflow_outputs'): from galaxy.managers.workflows import WorkflowContentsManager @@ -407,7 +407,7 @@ class SubWorkflowModule(WorkflowModule): tooltip=False) for order_index in sorted(subworkflow_dict['steps']): step = subworkflow_dict['steps'][order_index] - data_outputs = subworkflow_dict['steps'][order_index]['data_outputs'] + data_outputs = subworkflow_dict['steps'][order_index]['outputs'] for workflow_output in step['workflow_outputs']: label = workflow_output['label'] if not label: @@ -502,7 +502,7 @@ class InputModule(WorkflowModule): state.inputs = dict(input=None) return state - def get_data_inputs(self): + def get_all_inputs(self, data_only=False, connectable_only=False): return [] def execute(self, trans, progress, invocation_step, use_cached_job=False): @@ -542,7 +542,7 @@ class InputDataModule(InputModule): type = "data_input" name = "Input dataset" - def get_data_outputs(self): + def get_all_outputs(self, data_only=False): return [dict(name='output', extensions=['input'])] def get_filter_set(self, connections=None): @@ -583,7 +583,7 @@ class InputDataCollectionModule(InputModule): input_element = Element("param", name="input", label=self.label, type="data_collection", collection_type=collection_type) return dict(input=DataCollectionToolParameter(None, input_element, self.trans)) - def get_data_outputs(self): + def get_all_outputs(self, data_only=False): return [ dict( name='output', @@ -645,11 +645,16 @@ class InputParameterModule(WorkflowModule): state.inputs = dict(input=None) return state - def get_data_inputs(self): - return [self.state.inputs] + def get_all_outputs(self, data_only=False): + if data_only: + return [] - def get_input_parameters(self): - return [dict(name=self.name, label=self.label, type=self.state.inputs.get('parameter_type', self.parameter_type), optional=self.state.inputs.get('optional', self.optional))] + return [dict( + name='output', + label=self.label, + type=self.state.inputs.get('parameter_type', self.parameter_type), + parameter=True, + )] def execute(self, trans, progress, invocation_step, use_cached_job=False): step = invocation_step.workflow_step @@ -664,7 +669,7 @@ class PauseModule(WorkflowModule): type = "pause" name = "Pause for dataset review" - def get_data_inputs(self): + def get_all_inputs(self, data_only=False, connectable_only=False): input = dict( name="input", label="Dataset for Review", @@ -672,9 +677,9 @@ class PauseModule(WorkflowModule): extensions='input', input_type="dataset", ) - return [input] + return [input] if not data_only else [] - def get_data_outputs(self): + def get_all_outputs(self, data_only=False): return [dict(name="output", label="Reviewed Dataset", extensions=['input'])] def get_runtime_state(self): @@ -816,14 +821,24 @@ class ToolModule(WorkflowModule): def get_inputs(self): return self.tool.inputs if self.tool else {} - def get_all_inputs(self, data_only=False): + def get_all_inputs(self, data_only=False, connectable_only=False): + if data_only and connectable_only: + raise Exception("Must specify at most one of data_only and connectable_only as True.") + inputs = [] if self.tool: - def callback(input, prefixed_name, prefixed_label, **kwargs): + def callback(input, prefixed_name, prefixed_label, value=None, **kwargs): visible = not hasattr(input, 'hidden') or not input.hidden + input_type = input.type is_data = isinstance(input, DataToolParameter) or isinstance(input, DataCollectionToolParameter) - skip = data_only and (not visible or not is_data) + is_connectable = is_runtime_value(value) and runtime_to_json(value)["__class__"] == "ConnectedValue" + if data_only: + skip = not visible or not is_data + elif connectable_only: + skip = not visible or not (is_data or is_connectable) + else: + skip = not visible if not skip: if isinstance(input, DataToolParameter): inputs.append(dict( @@ -848,16 +863,14 @@ class ToolModule(WorkflowModule): label=prefixed_label, multiple=False, input_type="parameter", + type=input_type, ) ) visit_input_values(self.tool.inputs, self.state.inputs, callback) return inputs - def get_data_inputs(self): - return self.get_all_inputs(data_only=True) - - def get_data_outputs(self): + def get_all_outputs(self, data_only=False): data_outputs = [] if self.tool: for name, tool_output in self.tool.outputs.items(): diff --git a/lib/galaxy/workflow/run.py b/lib/galaxy/workflow/run.py index e696c3d86a7..65e04f0f1b6 100644 --- a/lib/galaxy/workflow/run.py +++ b/lib/galaxy/workflow/run.py @@ -352,13 +352,10 @@ class WorkflowProgress(object): try: replacement = step_outputs[output_name] except KeyError: - if is_data: - # Must resolve. - template = "Workflow evaluation problem - failed to find output_name %s in step_outputs %s" - message = template % (output_name, step_outputs) - raise Exception(message) - else: - replacement = modules.NO_REPLACEMENT + # Must resolve. + template = "Workflow evaluation problem - failed to find output_name %s in step_outputs %s" + message = template % (output_name, step_outputs) + raise Exception(message) if isinstance(replacement, model.HistoryDatasetCollectionAssociation): if not replacement.collection.populated: if not replacement.collection.waiting_for_elements: diff --git a/test/api/test_workflows.py b/test/api/test_workflows.py index 0222578eb2f..81c737ff2cb 100644 --- a/test/api/test_workflows.py +++ b/test/api/test_workflows.py @@ -468,8 +468,8 @@ class WorkflowsApiTestCase(BaseWorkflowsApiTestCase): 'name', 'tool_state', 'tooltip', - 'data_inputs', - 'data_outputs', + 'inputs', + 'outputs', 'config_form', 'annotation', 'post_job_actions', @@ -494,7 +494,7 @@ steps: downloaded_workflow = self._download_workflow(workflow_id, style="editor") steps = downloaded_workflow['steps'] assert len(steps) == 2 - assert steps['1']['data_outputs'][0]['collection_type'] == 'list:paired' + assert steps['1']['outputs'][0]['collection_type'] == 'list:paired' @skip_without_tool('collection_type_source') def test_export_editor_subworkflow_collection_type_source(self): @@ -525,7 +525,7 @@ steps: steps = downloaded_workflow['steps'] assert len(steps) == 2 assert steps['1']['type'] == 'subworkflow' - assert steps['1']['data_outputs'][0]['collection_type'] == 'list:paired' + assert steps['1']['outputs'][0]['collection_type'] == 'list:paired' def test_import_missing_tool(self): workflow = self.workflow_populator.load_workflow_from_resource(name="test_workflow_missing_tool") From 5a99ebd5af9d68f028b86d295846a1c4cefccfb4 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 24 Oct 2018 14:36:43 -0400 Subject: [PATCH 18/20] Remove filtering of connections for workflow editor. Things not matching should be handled there. --- lib/galaxy/managers/workflows.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index f03fa92d17f..c447a0b4eb6 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -598,9 +598,6 @@ class WorkflowContentsManager(UsesAnnotations): if isinstance(input, DataCollectionToolParameter): input_connections_type[input.name] = "dataset_collection" visit_input_values(module.tool.inputs, module.state.inputs, callback) - # Filter - # FIXME: this removes connection without displaying a message currently! - input_connections = [conn for conn in input_connections if conn.input_name in data_input_names or conn.non_data_connection] # post_job_actions pja_dict = {} for pja in step.post_job_actions: From 29950be83f8240acba05685b4a057b38d803b445 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Oct 2018 09:17:00 -0400 Subject: [PATCH 19/20] Workflow editor testing for step parameters/connections. - Fixes and refactoring for existing input tests (toward reuse). - Add simple test for non-data input. --- test/galaxy_selenium/navigation.yml | 3 + test/selenium_tests/test_workflow_editor.py | 101 +++++++++++++++++--- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml index 8e333d812a8..e39dfedd66c 100644 --- a/test/galaxy_selenium/navigation.yml +++ b/test/galaxy_selenium/navigation.yml @@ -264,6 +264,9 @@ workflow_editor: tool_menu_section_link: '#title___workflow__${section_name}__ a span' tool_menu_item_link: 'a#tool-menu-${section_name}-${item_name}' + connect_icon: 'div.ui-form-element[tour_id="${name}"] .ui-form-connected-icon' + collapse_icon: 'div.ui-form-element[tour_id="${name}"] .ui-form-collapsible-icon' + label_input: "[tour_id='__label'] input" annotation_input: "[tour_id='__annotation'] textarea" diff --git a/test/selenium_tests/test_workflow_editor.py b/test/selenium_tests/test_workflow_editor.py index 61ffd23d03e..a014ca9ebf7 100644 --- a/test/selenium_tests/test_workflow_editor.py +++ b/test/selenium_tests/test_workflow_editor.py @@ -54,10 +54,7 @@ class WorkflowEditorTestCase(SeleniumTestCase): editor = self.components.workflow_editor name = self.workflow_create_new() - editor.canvas_body.wait_for_visible() - editor.tool_menu.wait_for_visible() - editor.tool_menu_section_link(section_name="inputs").wait_for_and_click() - editor.tool_menu_item_link(section_name="inputs", item_name="data_input").wait_for_and_click() + self.workflow_editor_add_input(item_name="data_input") self.screenshot("workflow_editor_data_input_new") editor.label_input.wait_for_and_send_keys("input1") editor.annotation_input.wait_for_and_send_keys("my cool annotation") @@ -85,16 +82,13 @@ class WorkflowEditorTestCase(SeleniumTestCase): editor = self.components.workflow_editor name = self.workflow_create_new() - editor.canvas_body.wait_for_visible() - editor.tool_menu.wait_for_visible() - editor.tool_menu_section_link(section_name="inputs").wait_for_and_click() - editor.tool_menu_item_link(section_name="inputs", item_name="data_collection_input").wait_for_and_click() - self.screenshot("workflow_editor_data_input_collection_new") + self.workflow_editor_add_input(item_name="data_collection_input") + self.screenshot("workflow_editor_data_collection_input_new") editor.label_input.wait_for_and_send_keys("input1") editor.annotation_input.wait_for_and_send_keys("my cool annotation") editor.label_input.wait_for_and_click() # Seems to help force the save of whole annotation. self.sleep_for(self.wait_types.UX_RENDER) - self.screenshot("workflow_editor_data_input_collection_filled_in") + self.screenshot("workflow_editor_data_collection_input_filled_in") self.workflow_editor_save_and_close() self.workflow_index_open_with_name(name) data_input_node = editor.node._(label="input1") @@ -107,7 +101,82 @@ class WorkflowEditorTestCase(SeleniumTestCase): data_input_node.destroy.wait_for_and_click() data_input_node.wait_for_absent() - self.screenshot("workflow_editor_data_input_deleted") + self.screenshot("workflow_editor_data_collection_input_deleted") + + @selenium_test + def test_integer_input(self): + editor = self.components.workflow_editor + + name = self.workflow_create_new() + self.workflow_editor_add_input(item_name="parameter_input") + self.screenshot("workflow_editor_parameter_input_new") + + editor.label_input.wait_for_and_send_keys("input1") + editor.annotation_input.wait_for_and_send_keys("my cool annotation") + editor.label_input.wait_for_and_click() # Seems to help force the save of whole annotation. + self.sleep_for(self.wait_types.UX_RENDER) + self.screenshot("workflow_editor_parameter_input_filled_in") + self.workflow_editor_save_and_close() + self.workflow_index_open_with_name(name) + data_input_node = editor.node._(label="input1") + data_input_node.title.wait_for_and_click() + + label = editor.label_input.wait_for_value() + assert label == "input1", label + # should work but Galaxy is broken. + # assert editor.annotation_input.wait_for_value() == "my cool annotation" + + data_input_node.destroy.wait_for_and_click() + data_input_node.wait_for_absent() + self.screenshot("workflow_editor_parameter_input_deleted") + + @selenium_test + def test_non_data_connections(self): + self.open_in_workflow_editor(""" +class: GalaxyWorkflow +inputs: + input_int: integer +steps: + simple_constructs: + tool_id: simple_constructs + label: tool_exec + in: + inttest: input_int +""") + self.screenshot("workflow_editor_parameter_connection_simple") + self.assert_connected("input_int#output", "simple_constructs#inttest") + + editor = self.components.workflow_editor + + tool_node = editor.node._(label="simple_constructs") + tool_input = tool_node.input_terminal(name="inttest") + tool_input.wait_for_and_click() + + editor.connector_destroy_callout.wait_for_and_click() + self.assert_not_connected("input_int#output", "simple_constructs#inttest") + self.screenshot("workflow_editor_parameter_connection_destroyed") + + # When connected, cannot turn it into a RuntimeValue.. + collapse_input = editor.collapse_icon(name="inttest") + collapse_input.wait_for_absent_or_hidden() + + # If it is disconnected, then can specify as RuntimeValue + connect_icon = editor.connect_icon(name="inttest") + connect_icon.wait_for_visible() + connect_icon.wait_for_and_click() + collapse_input.wait_for_visible() + + # Also the connector should disappear + tool_input.wait_for_absent_or_hidden() + + # Now make it connected again and watch the requestss + connect_icon.wait_for_and_click() + + tool_input.wait_for_visible() + collapse_input.wait_for_absent_or_hidden() + + self.workflow_editor_connect("input_int#output", "simple_constructs#inttest", screenshot_partial="workflow_editor_parameter_connection_dragging") + self.assert_connected("input_int#output", "simple_constructs#inttest") @selenium_test def test_existing_connections(self): @@ -329,6 +398,16 @@ steps: return source_id, sink_id + def workflow_editor_add_input(self, item_name="data_input"): + editor = self.components.workflow_editor + + # Make sure we're on the the workflow editor and not clicking the main tool panel. + editor.canvas_body.wait_for_visible() + + editor.tool_menu.wait_for_visible() + editor.tool_menu_section_link(section_name="inputs").wait_for_and_click() + editor.tool_menu_item_link(section_name="inputs", item_name=item_name).wait_for_and_click() + def workflow_editor_destroy_connection(self, sink): editor = self.components.workflow_editor From f12e548872b0087b4ba8b5828bc80b6aa10207e4 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 5 Nov 2018 21:26:23 -0500 Subject: [PATCH 20/20] Step parameter workflow run test case and fix. --- lib/galaxy/workflow/run.py | 5 ++-- test/galaxy_selenium/navigation.yml | 3 +- test/selenium_tests/test_workflow_run.py | 35 +++++++++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/workflow/run.py b/lib/galaxy/workflow/run.py index 65e04f0f1b6..b75bee7ecac 100644 --- a/lib/galaxy/workflow/run.py +++ b/lib/galaxy/workflow/run.py @@ -387,11 +387,12 @@ class WorkflowProgress(object): if self.inputs_by_step_id: step_id = step.id - if step_id not in self.inputs_by_step_id: + if step_id not in self.inputs_by_step_id and 'output' not in outputs: template = "Step with id %s not found in inputs_step_id (%s)" message = template % (step_id, self.inputs_by_step_id) raise ValueError(message) - outputs['output'] = self.inputs_by_step_id[step_id] + elif step_id in self.inputs_by_step_id: + outputs['output'] = self.inputs_by_step_id[step_id] self.set_step_outputs(invocation_step, outputs) diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml index e39dfedd66c..c93b81b337d 100644 --- a/test/galaxy_selenium/navigation.yml +++ b/test/galaxy_selenium/navigation.yml @@ -236,7 +236,8 @@ workflows: workflow_run: selectors: - input_div: "[step-label='${label}'] .select2-container" + input_div: "[step-label='${label}']" + input_data_div: "[step-label='${label}'] .select2-container" # TODO: put step labels in the DOM ideally subworkflow_step_icon: ".portlet-title-icon.fa-sitemap + span" diff --git a/test/selenium_tests/test_workflow_run.py b/test/selenium_tests/test_workflow_run.py index 60f9913fdc3..cc9d1026147 100644 --- a/test/selenium_tests/test_workflow_run.py +++ b/test/selenium_tests/test_workflow_run.py @@ -96,6 +96,39 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions): details = self.dataset_populator.get_history_dataset_details(history_id, hid=output_hid) assert details["name"] == "moocow suffix", details + @selenium_test + @managed_history + def test_step_parameter_inputs(self): + self.perform_upload(self.get_filename("1.txt")) + self.wait_for_history() + self.open_in_workflow_run(""" +class: GalaxyWorkflow +inputs: + input_int: integer + input_data: data +steps: + simple_constructs: + tool_id: simple_constructs + label: tool_exec + in: + inttest: input_int + files_0|file: input_data +""") + workflow_run = self.components.workflow_run + input_div_element = workflow_run.input_div(label="input_int").wait_for_visible() + input_element = input_div_element.find_element_by_css_selector("input") + input_element.clear() + input_element.send_keys("12345") + + self.screenshot("workflow_run_step_parameter_input") + self.workflow_run_submit() + output_hid = 2 + self.history_panel_wait_for_hid_ok(output_hid, allowed_force_refreshes=1) + history_id = self.current_history_id() + content = self.dataset_populator.get_history_dataset_content(history_id, hid=output_hid) + assert "12345" in content, content + assert "chr6_hla_hap2" in content + @selenium_test @managed_history def test_replacement_parameters_on_subworkflows(self): @@ -160,7 +193,7 @@ class WorkflowRunTestCase(SeleniumTestCase, UsesHistoryItemAssertions): def workflow_run_specify_inputs(self, inputs): workflow_run = self.components.workflow_run for label, value in inputs.items(): - input_div_element = workflow_run.input_div(label=label).wait_for_visible() + input_div_element = workflow_run.input_data_div(label=label).wait_for_visible() self.select2_set_value(input_div_element, "%d: " % value["hid"]) def workflow_run_with_name(self, name):