mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
753 lines
20 KiB
JavaScript
753 lines
20 KiB
JavaScript
/**
|
|
* Model, view, and controller objects for Galaxy tools and tool panel.
|
|
*/
|
|
|
|
define( ["libs/underscore", "viz/trackster/util", "mvc/data" ],
|
|
function(_, util, data) {
|
|
|
|
/**
|
|
* Mixin for tracking model visibility.
|
|
*/
|
|
var VisibilityMixin = {
|
|
hidden: false,
|
|
|
|
show: function() {
|
|
this.set("hidden", false);
|
|
},
|
|
|
|
hide: function() {
|
|
this.set("hidden", true);
|
|
},
|
|
|
|
toggle: function() {
|
|
this.set("hidden", !this.get("hidden"));
|
|
},
|
|
|
|
is_visible: function() {
|
|
return !this.attributes.hidden;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* A tool parameter.
|
|
*/
|
|
var ToolParameter = Backbone.Model.extend({
|
|
defaults: {
|
|
name: null,
|
|
label: null,
|
|
type: null,
|
|
value: null,
|
|
html: null,
|
|
num_samples: 5
|
|
},
|
|
|
|
initialize: function() {
|
|
this.attributes.html = unescape(this.attributes.html);
|
|
},
|
|
|
|
copy: function() {
|
|
return new ToolParameter(this.toJSON());
|
|
},
|
|
|
|
set_value: function(value) {
|
|
this.set('value', value || '');
|
|
}
|
|
});
|
|
|
|
var ToolParameterCollection = Backbone.Collection.extend({
|
|
model: ToolParameter
|
|
});
|
|
|
|
/**
|
|
* A data tool parameter.
|
|
*/
|
|
var DataToolParameter = ToolParameter.extend({});
|
|
|
|
/**
|
|
* An integer tool parameter.
|
|
*/
|
|
var IntegerToolParameter = ToolParameter.extend({
|
|
set_value: function(value) {
|
|
this.set('value', parseInt(value, 10));
|
|
},
|
|
|
|
/**
|
|
* Returns samples from a tool input.
|
|
*/
|
|
get_samples: function() {
|
|
return d3.scale.linear()
|
|
.domain([this.get('min'), this.get('max')])
|
|
.ticks(this.get('num_samples'));
|
|
}
|
|
});
|
|
|
|
var FloatToolParameter = IntegerToolParameter.extend({
|
|
set_value: function(value) {
|
|
this.set('value', parseFloat(value));
|
|
}
|
|
});
|
|
|
|
/**
|
|
* A select tool parameter.
|
|
*/
|
|
var SelectToolParameter = ToolParameter.extend({
|
|
/**
|
|
* Returns tool options.
|
|
*/
|
|
get_samples: function() {
|
|
return _.map(this.get('options'), function(option) {
|
|
return option[0];
|
|
});
|
|
}
|
|
});
|
|
|
|
// Set up dictionary of parameter types.
|
|
ToolParameter.subModelTypes = {
|
|
'integer': IntegerToolParameter,
|
|
'float': FloatToolParameter,
|
|
'data': DataToolParameter,
|
|
'select': SelectToolParameter
|
|
};
|
|
|
|
/**
|
|
* A Galaxy tool.
|
|
*/
|
|
var Tool = Backbone.Model.extend({
|
|
// Default attributes.
|
|
defaults: {
|
|
id: null,
|
|
name: null,
|
|
description: null,
|
|
target: null,
|
|
inputs: [],
|
|
outputs: []
|
|
},
|
|
|
|
initialize: function(options) {
|
|
|
|
// Set parameters.
|
|
this.set('inputs', new ToolParameterCollection(_.map(options.inputs, function(p) {
|
|
var p_class = ToolParameter.subModelTypes[p.type] || ToolParameter;
|
|
return new p_class(p);
|
|
})));
|
|
},
|
|
|
|
urlRoot: galaxy_config.root + 'api/tools',
|
|
|
|
/**
|
|
* Removes inputs of a particular type; this is useful because not all inputs can be handled by
|
|
* client and server yet.
|
|
*/
|
|
remove_inputs: function(types) {
|
|
var tool = this,
|
|
incompatible_inputs = tool.get('inputs').filter( function(input) {
|
|
return ( types.indexOf( input.get('type') ) !== -1);
|
|
});
|
|
tool.get('inputs').remove(incompatible_inputs);
|
|
},
|
|
|
|
/**
|
|
* Returns object copy, optionally including only inputs that can be sampled.
|
|
*/
|
|
copy: function(only_samplable_inputs) {
|
|
var copy = new Tool(this.toJSON());
|
|
|
|
// Return only samplable inputs if flag is set.
|
|
if (only_samplable_inputs) {
|
|
var valid_inputs = new Backbone.Collection();
|
|
copy.get('inputs').each(function(input) {
|
|
if (input.get_samples()) {
|
|
valid_inputs.push(input);
|
|
}
|
|
});
|
|
copy.set('inputs', valid_inputs);
|
|
}
|
|
|
|
return copy;
|
|
},
|
|
|
|
apply_search_results: function(results) {
|
|
( _.indexOf(results, this.attributes.id) !== -1 ? this.show() : this.hide() );
|
|
return this.is_visible();
|
|
},
|
|
|
|
/**
|
|
* Set a tool input's value.
|
|
*/
|
|
set_input_value: function(name, value) {
|
|
this.get('inputs').find(function(input) {
|
|
return input.get('name') === name;
|
|
}).set('value', value);
|
|
},
|
|
|
|
/**
|
|
* Set many input values at once.
|
|
*/
|
|
set_input_values: function(inputs_dict) {
|
|
var self = this;
|
|
_.each(_.keys(inputs_dict), function(input_name) {
|
|
self.set_input_value(input_name, inputs_dict[input_name]);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Run tool; returns a Deferred that resolves to the tool's output(s).
|
|
*/
|
|
run: function() {
|
|
return this._run();
|
|
},
|
|
|
|
/**
|
|
* Rerun tool using regions and a target dataset.
|
|
*/
|
|
rerun: function(target_dataset, regions) {
|
|
return this._run({
|
|
action: 'rerun',
|
|
target_dataset_id: target_dataset.id,
|
|
regions: regions
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns input dict for tool's inputs.
|
|
*/
|
|
get_inputs_dict: function() {
|
|
var input_dict = {};
|
|
this.get('inputs').each(function(input) {
|
|
input_dict[input.get('name')] = input.get('value');
|
|
});
|
|
return input_dict;
|
|
},
|
|
|
|
/**
|
|
* Run tool; returns a Deferred that resolves to the tool's output(s).
|
|
* NOTE: this method is a helper method and should not be called directly.
|
|
*/
|
|
_run: function(additional_params) {
|
|
// Create payload.
|
|
var payload = _.extend({
|
|
tool_id: this.id,
|
|
inputs: this.get_inputs_dict()
|
|
}, additional_params);
|
|
|
|
// Because job may require indexing datasets, use server-side
|
|
// deferred to ensure that job is run. Also use deferred that
|
|
// resolves to outputs from tool.
|
|
var run_deferred = $.Deferred(),
|
|
ss_deferred = new util.ServerStateDeferred({
|
|
ajax_settings: {
|
|
url: this.urlRoot,
|
|
data: JSON.stringify(payload),
|
|
dataType: "json",
|
|
contentType: 'application/json',
|
|
type: "POST"
|
|
},
|
|
interval: 2000,
|
|
success_fn: function(response) {
|
|
return response !== "pending";
|
|
}
|
|
});
|
|
|
|
// Run job and resolve run_deferred to tool outputs.
|
|
$.when(ss_deferred.go()).then(function(result) {
|
|
run_deferred.resolve(new data.DatasetCollection().reset(result));
|
|
});
|
|
return run_deferred;
|
|
}
|
|
});
|
|
_.extend(Tool.prototype, VisibilityMixin);
|
|
|
|
/**
|
|
* Tool view.
|
|
*/
|
|
var ToolView = Backbone.View.extend({
|
|
|
|
});
|
|
|
|
/**
|
|
* Wrap collection of tools for fast access/manipulation.
|
|
*/
|
|
var ToolCollection = Backbone.Collection.extend({
|
|
model: Tool
|
|
});
|
|
|
|
/**
|
|
* Label or section header in tool panel.
|
|
*/
|
|
var ToolSectionLabel = Backbone.Model.extend(VisibilityMixin);
|
|
|
|
/**
|
|
* Section of tool panel with elements (labels and tools).
|
|
*/
|
|
var ToolSection = Backbone.Model.extend({
|
|
defaults: {
|
|
elems: [],
|
|
open: false
|
|
},
|
|
|
|
clear_search_results: function() {
|
|
_.each(this.attributes.elems, function(elt) {
|
|
elt.show();
|
|
});
|
|
|
|
this.show();
|
|
this.set("open", false);
|
|
},
|
|
|
|
apply_search_results: function(results) {
|
|
var all_hidden = true,
|
|
cur_label;
|
|
_.each(this.attributes.elems, function(elt) {
|
|
if (elt instanceof ToolSectionLabel) {
|
|
cur_label = elt;
|
|
cur_label.hide();
|
|
}
|
|
else if (elt instanceof Tool) {
|
|
if (elt.apply_search_results(results)) {
|
|
all_hidden = false;
|
|
if (cur_label) {
|
|
cur_label.show();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (all_hidden) {
|
|
this.hide();
|
|
}
|
|
else {
|
|
this.show();
|
|
this.set("open", true);
|
|
}
|
|
}
|
|
});
|
|
_.extend(ToolSection.prototype, VisibilityMixin);
|
|
|
|
/**
|
|
* Tool search that updates results when query is changed. Result value of null
|
|
* indicates that query was not run; if not null, results are from search using
|
|
* query.
|
|
*/
|
|
var ToolSearch = Backbone.Model.extend({
|
|
defaults: {
|
|
search_hint_string: "search tools",
|
|
min_chars_for_search: 3,
|
|
spinner_url: "",
|
|
clear_btn_url: "",
|
|
search_url: "",
|
|
visible: true,
|
|
query: "",
|
|
results: null,
|
|
// ESC (27) will clear the input field and tool search filters
|
|
clear_key: 27
|
|
},
|
|
|
|
initialize: function() {
|
|
this.on("change:query", this.do_search);
|
|
},
|
|
|
|
/**
|
|
* Do the search and update the results.
|
|
*/
|
|
do_search: function() {
|
|
var query = this.attributes.query;
|
|
|
|
// If query is too short, do not search.
|
|
if (query.length < this.attributes.min_chars_for_search) {
|
|
this.set("results", null);
|
|
return;
|
|
}
|
|
|
|
// Do search via AJAX.
|
|
var q = query + '*';
|
|
// Stop previous ajax-request
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
}
|
|
// Start a new ajax-request in X ms
|
|
$("#search-clear-btn").hide();
|
|
$("#search-spinner").show();
|
|
var self = this;
|
|
this.timer = setTimeout(function () {
|
|
$.get(self.attributes.search_url, { query: q }, function (data) {
|
|
self.set("results", data);
|
|
$("#search-spinner").hide();
|
|
$("#search-clear-btn").show();
|
|
}, "json" );
|
|
}, 200 );
|
|
},
|
|
|
|
clear_search: function() {
|
|
this.set("query", "");
|
|
this.set("results", null);
|
|
}
|
|
|
|
});
|
|
_.extend(ToolSearch.prototype, VisibilityMixin);
|
|
|
|
/**
|
|
* Tool Panel.
|
|
*/
|
|
var ToolPanel = Backbone.Model.extend({
|
|
|
|
initialize: function(options) {
|
|
this.attributes.tool_search = options.tool_search;
|
|
this.attributes.tool_search.on("change:results", this.apply_search_results, this);
|
|
this.attributes.tools = options.tools;
|
|
this.attributes.layout = new Backbone.Collection( this.parse(options.layout) );
|
|
},
|
|
|
|
/**
|
|
* Parse tool panel dictionary and return collection of tool panel elements.
|
|
*/
|
|
parse: function(response) {
|
|
// Recursive function to parse tool panel elements.
|
|
var self = this,
|
|
// Helper to recursively parse tool panel.
|
|
parse_elt = function(elt_dict) {
|
|
var type = elt_dict.model_class;
|
|
// There are many types of tools; for now, anything that ends in 'Tool'
|
|
// is treated as a generic tool.
|
|
if ( type.indexOf('Tool') === type.length - 4 ) {
|
|
return self.attributes.tools.get(elt_dict.id);
|
|
}
|
|
else if (type === 'ToolSection') {
|
|
// Parse elements.
|
|
var elems = _.map(elt_dict.elems, parse_elt);
|
|
elt_dict.elems = elems;
|
|
return new ToolSection(elt_dict);
|
|
}
|
|
else if (type === 'ToolSectionLabel') {
|
|
return new ToolSectionLabel(elt_dict);
|
|
}
|
|
};
|
|
|
|
return _.map(response, parse_elt);
|
|
},
|
|
|
|
clear_search_results: function() {
|
|
this.get('layout').each(function(panel_elt) {
|
|
if (panel_elt instanceof ToolSection) {
|
|
panel_elt.clear_search_results();
|
|
}
|
|
else {
|
|
// Label or tool, so just show.
|
|
panel_elt.show();
|
|
}
|
|
});
|
|
},
|
|
|
|
apply_search_results: function() {
|
|
var results = this.get('tool_search').get('results');
|
|
if (results === null) {
|
|
this.clear_search_results();
|
|
return;
|
|
}
|
|
|
|
var cur_label = null;
|
|
this.get('layout').each(function(panel_elt) {
|
|
if (panel_elt instanceof ToolSectionLabel) {
|
|
cur_label = panel_elt;
|
|
cur_label.hide();
|
|
}
|
|
else if (panel_elt instanceof Tool) {
|
|
if (panel_elt.apply_search_results(results)) {
|
|
if (cur_label) {
|
|
cur_label.show();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Starting new section, so clear current label.
|
|
cur_label = null;
|
|
panel_elt.apply_search_results(results);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* View classes for Galaxy tools and tool panel.
|
|
*
|
|
* Views use precompiled Handlebars templates for rendering. Views update as needed
|
|
* based on (a) model/collection events and (b) user interactions; in this sense,
|
|
* they are controllers are well and the HTML is the real view in the MVC architecture.
|
|
*/
|
|
|
|
/**
|
|
* Base view that handles visibility based on model's hidden attribute.
|
|
*/
|
|
var BaseView = Backbone.View.extend({
|
|
initialize: function() {
|
|
this.model.on("change:hidden", this.update_visible, this);
|
|
this.update_visible();
|
|
},
|
|
update_visible: function() {
|
|
( this.model.attributes.hidden ? this.$el.hide() : this.$el.show() );
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Link to a tool.
|
|
*/
|
|
var ToolLinkView = BaseView.extend({
|
|
tagName: 'div',
|
|
|
|
render: function() {
|
|
this.$el.append( Handlebars.templates.tool_link(this.model.toJSON()) );
|
|
return this;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Panel label/section header.
|
|
*/
|
|
var ToolSectionLabelView = BaseView.extend({
|
|
tagName: 'div',
|
|
className: 'toolPanelLabel',
|
|
|
|
render: function() {
|
|
this.$el.append( $("<span/>").text(this.model.attributes.text) );
|
|
return this;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Panel section.
|
|
*/
|
|
var ToolSectionView = BaseView.extend({
|
|
tagName: 'div',
|
|
className: 'toolSectionWrapper',
|
|
|
|
initialize: function() {
|
|
BaseView.prototype.initialize.call(this);
|
|
this.model.on("change:open", this.update_open, this);
|
|
},
|
|
|
|
render: function() {
|
|
// Build using template.
|
|
this.$el.append( Handlebars.templates.panel_section(this.model.toJSON()) );
|
|
|
|
// Add tools to section.
|
|
var section_body = this.$el.find(".toolSectionBody");
|
|
_.each(this.model.attributes.elems, function(elt) {
|
|
if (elt instanceof Tool) {
|
|
var tool_view = new ToolLinkView({model: elt, className: "toolTitle"});
|
|
tool_view.render();
|
|
section_body.append(tool_view.$el);
|
|
}
|
|
else if (elt instanceof ToolSectionLabel) {
|
|
var label_view = new ToolSectionLabelView({model: elt});
|
|
label_view.render();
|
|
section_body.append(label_view.$el);
|
|
}
|
|
else {
|
|
// TODO: handle nested section bodies?
|
|
}
|
|
});
|
|
return this;
|
|
},
|
|
|
|
events: {
|
|
'click .toolSectionTitle > a': 'toggle'
|
|
},
|
|
|
|
/**
|
|
* Toggle visibility of tool section.
|
|
*/
|
|
toggle: function() {
|
|
this.model.set("open", !this.model.attributes.open);
|
|
},
|
|
|
|
/**
|
|
* Update whether section is open or close.
|
|
*/
|
|
update_open: function() {
|
|
(this.model.attributes.open ?
|
|
this.$el.children(".toolSectionBody").slideDown("fast") :
|
|
this.$el.children(".toolSectionBody").slideUp("fast")
|
|
);
|
|
}
|
|
});
|
|
|
|
var ToolSearchView = Backbone.View.extend({
|
|
tagName: 'div',
|
|
id: 'tool-search',
|
|
className: 'bar',
|
|
|
|
events: {
|
|
'click': 'focus_and_select',
|
|
'keyup :input': 'query_changed',
|
|
'click #search-clear-btn': 'clear'
|
|
},
|
|
|
|
render: function() {
|
|
this.$el.append( Handlebars.templates.tool_search(this.model.toJSON()) );
|
|
if (!this.model.is_visible()) {
|
|
this.$el.hide();
|
|
}
|
|
this.$el.find('[title]').tooltip();
|
|
return this;
|
|
},
|
|
|
|
focus_and_select: function() {
|
|
this.$el.find(":input").focus().select();
|
|
},
|
|
|
|
clear: function() {
|
|
this.model.clear_search();
|
|
this.$el.find(":input").val(this.model.attributes.search_hint_string);
|
|
this.focus_and_select();
|
|
return false;
|
|
},
|
|
|
|
query_changed: function( evData ) {
|
|
// check for the 'clear key' (ESC) first
|
|
if( ( this.model.attributes.clear_key ) &&
|
|
( this.model.attributes.clear_key === evData.which ) ){
|
|
this.clear();
|
|
return false;
|
|
}
|
|
this.model.set("query", this.$el.find(":input").val());
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Tool panel view. Events triggered include:
|
|
* tool_link_click(click event, tool_model)
|
|
*/
|
|
var ToolPanelView = Backbone.View.extend({
|
|
tagName: 'div',
|
|
className: 'toolMenu',
|
|
|
|
/**
|
|
* Set up view.
|
|
*/
|
|
initialize: function() {
|
|
this.model.get('tool_search').on("change:results", this.handle_search_results, this);
|
|
},
|
|
|
|
render: function() {
|
|
var self = this;
|
|
|
|
// Render search.
|
|
var search_view = new ToolSearchView( { model: this.model.get('tool_search') } );
|
|
search_view.render();
|
|
self.$el.append(search_view.$el);
|
|
|
|
// Render panel.
|
|
this.model.get('layout').each(function(panel_elt) {
|
|
if (panel_elt instanceof ToolSection) {
|
|
var section_title_view = new ToolSectionView({model: panel_elt});
|
|
section_title_view.render();
|
|
self.$el.append(section_title_view.$el);
|
|
}
|
|
else if (panel_elt instanceof Tool) {
|
|
var tool_view = new ToolLinkView({model: panel_elt, className: "toolTitleNoSection"});
|
|
tool_view.render();
|
|
self.$el.append(tool_view.$el);
|
|
}
|
|
else if (panel_elt instanceof ToolSectionLabel) {
|
|
var label_view = new ToolSectionLabelView({model: panel_elt});
|
|
label_view.render();
|
|
self.$el.append(label_view.$el);
|
|
}
|
|
});
|
|
|
|
// Setup tool link click eventing.
|
|
self.$el.find("a.tool-link").click(function(e) {
|
|
// Tool id is always the first class.
|
|
var
|
|
tool_id = $(this).attr('class').split(/\s+/)[0],
|
|
tool = self.model.get('tools').get(tool_id);
|
|
|
|
self.trigger("tool_link_click", e, tool);
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
handle_search_results: function() {
|
|
var results = this.model.get('tool_search').get('results');
|
|
if (results && results.length === 0) {
|
|
$("#search-no-results").show();
|
|
}
|
|
else {
|
|
$("#search-no-results").hide();
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* View for working with a tool: setting parameters and inputs and executing the tool.
|
|
*/
|
|
var ToolFormView = Backbone.View.extend({
|
|
className: 'toolForm',
|
|
|
|
render: function() {
|
|
this.$el.children().remove();
|
|
this.$el.append( Handlebars.templates.tool_form(this.model.toJSON()) );
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Integrated tool menu + tool execution.
|
|
*/
|
|
var IntegratedToolMenuAndView = Backbone.View.extend({
|
|
className: 'toolMenuAndView',
|
|
|
|
initialize: function() {
|
|
this.tool_panel_view = new ToolPanelView({collection: this.collection});
|
|
this.tool_form_view = new ToolFormView();
|
|
},
|
|
|
|
render: function() {
|
|
// Render and append tool panel.
|
|
this.tool_panel_view.render();
|
|
this.tool_panel_view.$el.css("float", "left");
|
|
this.$el.append(this.tool_panel_view.$el);
|
|
|
|
// Append tool form view.
|
|
this.tool_form_view.$el.hide();
|
|
this.$el.append(this.tool_form_view.$el);
|
|
|
|
// On tool link click, show tool.
|
|
var self = this;
|
|
this.tool_panel_view.on("tool_link_click", function(e, tool) {
|
|
// Prevents click from activating link:
|
|
e.preventDefault();
|
|
// Show tool that was clicked on:
|
|
self.show_tool(tool);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Fetch and display tool.
|
|
*/
|
|
show_tool: function(tool) {
|
|
var self = this;
|
|
tool.fetch().done( function() {
|
|
self.tool_form_view.model = tool;
|
|
self.tool_form_view.render();
|
|
self.tool_form_view.$el.show();
|
|
$('#left').width("650px");
|
|
});
|
|
}
|
|
});
|
|
|
|
// Exports
|
|
return {
|
|
ToolParameter: ToolParameter,
|
|
IntegerToolParameter: IntegerToolParameter,
|
|
SelectToolParameter: SelectToolParameter,
|
|
Tool: Tool,
|
|
ToolCollection: ToolCollection,
|
|
ToolSearch: ToolSearch,
|
|
ToolPanel: ToolPanel,
|
|
ToolPanelView: ToolPanelView,
|
|
ToolFormView: ToolFormView
|
|
};
|
|
|
|
});
|