Skip to content

Commit

Permalink
Allow rule based operations on existing collections.
Browse files Browse the repository at this point in the history
Allow the rules DSL & GUI component to operate on existing collections to allow filtering, sorting, modifying identifiers and general re-organization of existing collections (e.g. the outputs of tools). Implementing this as a collection operation tool so that it should be executable interactively in the tool form and in a batch fashion as part of workflow executions.

For this to be tracked properly as a tool execution and to work properly in the tool form, I've implemented a new tool framework and tool form parameter type called "rules".

This can thought of as a more GUI friendly alternative to my proposed collection operations that consumed JavaScript expressions.

This includes API tests for both tool and workflow execution of the new tool as well as Selenium tests for tool form execution, workflow editor interactions, and workflow running.
  • Loading branch information
jmchilton committed Apr 23, 2018
1 parent 784876e commit 7f64a99
Show file tree
Hide file tree
Showing 38 changed files with 1,981 additions and 197 deletions.
301 changes: 200 additions & 101 deletions client/galaxy/scripts/components/RuleCollectionBuilder.vue

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions client/galaxy/scripts/components/RulesDisplay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<template>
<div>
<ol class="rules">
<rule-display-preview
v-for="(rule, index) in rules"
v-bind:rule="rule"
v-bind:index="index"
v-bind:key="index"
:col-headers="columnData.colHeadersPerRule[index]" />
<identifier-display-preview v-for="(map, index) in mapping"
v-bind="map"
v-bind:index="index"
v-bind:key="map.type"
:col-headers="colHeaders" />
</ol>
</div>
</template>
<script>
import RuleDefs from "mvc/rules/rule-definitions";
import _l from "utils/localization";
// read-only variants of the components for displaying these rules and mappings in builder widget.
const RuleDisplayPreview = {
template: `
<li class="rule">
<span class="rule-display">{{ title }}
</span>
<span class="rule-warning" v-if="rule.warn">
{{ rule.warn }}
</span>
<span class="rule-error" v-if="rule.error">
<span class="alert-message">{{ rule.error }}</span>
</span>
</li>
`,
props: {
rule: {
required: true,
type: Object
},
colHeaders: {
type: Array,
required: false
}
},
computed: {
title() {
const ruleType = this.rule.type;
return RuleDefs.RULES[ruleType].display(this.rule, this.colHeaders);
}
},
methods: {}
};
const IdentifierDisplayPreview = {
template: `
<li class="rule" :title="help">
Set {{ columnsLabel }} as {{ typeDisplay }}
</li>
`,
props: {
type: {
type: String,
required: true
},
columns: {
required: true
},
colHeaders: {
type: Array,
required: true
}
},
computed: {
typeDisplay() {
return RuleDefs.MAPPING_TARGETS[this.type].label;
},
help() {
return RuleDefs.MAPPING_TARGETS[this.type].help || "";
},
columnsLabel() {
return RuleDefs.columnDisplay(this.columns, this.colHeaders);
}
}
};
export default {
data: function() {
return {};
},
computed: {
mapping: function() {
return this.inputRules ? this.inputRules.mapping : [];
},
rules: function() {
return this.inputRules ? this.inputRules.rules : [];
},
columnData: function() {
const colHeadersPerRule = [];
const hotData = RuleDefs.applyRules([], [], [], this.rules, colHeadersPerRule);
return { colHeadersPerRule: colHeadersPerRule, columns: hotData.columns };
},
colHeaders: function() {
const columns = this.columnData.columns;
return RuleDefs.colHeadersFor([], columns);
}
},
props: {
inputRules: {
required: false,
type: Object
}
},
components: {
RuleDisplayPreview,
IdentifierDisplayPreview
}
};
</script>
47 changes: 16 additions & 31 deletions client/galaxy/scripts/mocha/tests/rules_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,29 @@ function applyRules(rules, data, sources) {
columns.push("new");
}
}
for (var ruleIndex in rules) {
const rule = rules[ruleIndex];
rule.error = null;
rule.warn = null;

var ruleType = rule.type;
const ruleDef = RULES[ruleType];
const res = ruleDef.apply(rule, data, sources, columns);
if (res.error) {
throw res.error;
} else {
if (res.warn) {
rule.warn = res.warn;
}
data = res.data || data;
sources = res.sources || sources;
columns = res.columns || columns;
}
}
return { data, sources, columns };
return RuleDefs.applyRules(data, sources, columns, rules);
}

function itShouldConform(specTestCase, i) {
it("should pass conformance test case " + i, function() {
chai.assert.property(specTestCase, "rules");
chai.assert.property(specTestCase, "initial");
chai.assert.property(specTestCase, "final");
if (specTestCase.initial) {
chai.assert.property(specTestCase, "final");

const rules = specTestCase.rules;
const initial = specTestCase.initial;
const expectedFinal = specTestCase.final;
const rules = specTestCase.rules;
const initial = specTestCase.initial;
const expectedFinal = specTestCase.final;

const final = applyRules(rules, initial.data, initial.sources);
const finalData = final.data;
const finalSources = final.sources;
chai.assert.deepEqual(finalData, expectedFinal.data);
if (expectedFinal.sources !== undefined) {
chai.assert.deepEqual(finalSources, expectedFinal.sources);
const final = applyRules(rules, initial.data, initial.sources);
const finalData = final.data;
const finalSources = final.sources;
chai.assert.deepEqual(finalData, expectedFinal.data);
if (expectedFinal.sources !== undefined) {
chai.assert.deepEqual(finalSources, expectedFinal.sources);
}
} else {
chai.assert(specTestCase.error);
// TODO: test these...
}
});
}
Expand Down
10 changes: 7 additions & 3 deletions client/galaxy/scripts/mvc/collection/list-collection-creator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,9 @@ var ruleBasedCollectionCreatorModal = function _ruleBasedCollectionCreatorModal(
creationFn: options.creationFn,
oncancel: options.oncancel,
oncreate: options.oncreate,
defaultHideSourceItems: options.defaultHideSourceItems
defaultHideSourceItems: options.defaultHideSourceItems,
saveRulesFn: options.saveRulesFn,
initialRules: options.initialRules
}
}).$mount(vm);
return deferred;
Expand Down Expand Up @@ -1127,7 +1129,8 @@ function createListCollection(contents, defaultHideSourceItems) {

function createCollectionViaRules(selection, defaultHideSourceItems) {
let elements, elementsType, importType;
if (!selection.selectionType) {
const selectionType = selection.selectionType;
if (!selectionType) {
// Have HDAs from the history panel.
elements = selection.toJSON();
elementsType = "datasets";
Expand Down Expand Up @@ -1170,5 +1173,6 @@ export default {
collectionCreatorModal: collectionCreatorModal,
listCollectionCreatorModal: listCollectionCreatorModal,
createListCollection: createListCollection,
createCollectionViaRules: createCollectionViaRules
createCollectionViaRules: createCollectionViaRules,
ruleBasedCollectionCreatorModal: ruleBasedCollectionCreatorModal
};
13 changes: 11 additions & 2 deletions client/galaxy/scripts/mvc/form/form-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@ export default Backbone.View.extend({
// render visibility
this.$el[this.model.get("hidden") ? "hide" : "show"]();
// render preview view for collapsed fields
// allow at least newlines to render properly after escape
const html = _.escape(this.model.get("text_value")).replace(/\n/g, "<br />");
this.$preview[
(this.field.collapsed && this.model.get("collapsible_preview")) || this.model.get("disabled")
? "show"
: "hide"
]().html(_.escape(this.model.get("text_value")));
]().html(html);
// render error messages
var error_text = this.model.get("error_text");
this.$error[error_text ? "show" : "hide"]();
Expand All @@ -106,7 +108,14 @@ export default Backbone.View.extend({
style: this.model.get("style")
});
// render collapsible options
if (!this.model.get("disabled") && this.model.get("collapsible_value") !== undefined) {
const workflowRuntimeCompatible =
this.field.workflowRuntimeCompatible === undefined ? true : this.field.workflowRuntimeCompatible;
if (
workflowRuntimeCompatible &&
!this.model.get("disabled") &&
this.model.get("collapsible_value") !== undefined
) {
console.log(this.field);
var collapsible_state = this.field.collapsed ? "enable" : "disable";
this.$title_text.hide();
this.$collapsible.show();
Expand Down
11 changes: 10 additions & 1 deletion client/galaxy/scripts/mvc/form/form-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SelectContent from "mvc/ui/ui-select-content";
import SelectLibrary from "mvc/ui/ui-select-library";
import SelectFtp from "mvc/ui/ui-select-ftp";
import SelectGenomeSpace from "mvc/ui/ui-select-genomespace";
import RulesEdit from "mvc/ui/ui-rules-edit";
import ColorPicker from "mvc/ui/ui-color-picker";
// create form view
export default Backbone.Model.extend({
Expand All @@ -30,6 +31,7 @@ export default Backbone.Model.extend({
library_data: "_fieldLibrary",
ftpfile: "_fieldFtp",
upload: "_fieldUpload",
rules: "_fieldRulesEdit",
genomespacefile: "_fieldGenomeSpace"
},

Expand Down Expand Up @@ -215,13 +217,20 @@ export default Backbone.Model.extend({
/** GenomeSpace file select field
*/
_fieldGenomeSpace: function(input_def) {
var self = this;
return new SelectGenomeSpace.View({
id: `field-${input_def.id}`,
onchange: input_def.onchange
});
},

_fieldRulesEdit: function(input_def) {
return new RulesEdit.View({
id: `field-${input_def.id}`,
onchange: input_def.onchange,
target: input_def.target
});
},

/** Upload file field */
_fieldUpload: function(input_def) {
return new Ui.Upload({
Expand Down
5 changes: 4 additions & 1 deletion client/galaxy/scripts/mvc/form/form-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ export default Backbone.View.extend({
var self = this;
this.data.matchModel(new_model, (node, input_id) => {
var input = self.input_list[input_id];
var field = self.field_list[input_id];
if (field.refreshDefinition) {
field.refreshDefinition(node);
}
if (input && input.options) {
if (!_.isEqual(input.options, node.options)) {
input.options = node.options;
var field = self.field_list[input_id];
if (field.update) {
var new_options = [];
if (["data", "data_collection", "drill_down"].indexOf(input.type) != -1) {
Expand Down
Loading

0 comments on commit 7f64a99

Please sign in to comment.