PageRenderTime 56ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/client/galaxy/scripts/mvc/tool/tools.js

https://bitbucket.org/galaxy/galaxy-central/
JavaScript | 801 lines | 536 code | 102 blank | 163 comment | 45 complexity | f62877594d8a12fb0a493397c0dfb131 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. /**
  2. * Model, view, and controller objects for Galaxy tools and tool panel.
  3. */
  4. define([
  5. "libs/underscore",
  6. "viz/trackster/util",
  7. "mvc/dataset/data",
  8. "mvc/tool/tool-form",
  9. "templates/tool_form.handlebars",
  10. "templates/tool_link.handlebars",
  11. "templates/panel_section.handlebars",
  12. "templates/tool_search.handlebars",
  13. ], function(_, util, data, ToolForm, tool_form_template, tool_link_template, panel_section_template, tool_search_template) {
  14. /**
  15. * Mixin for tracking model visibility.
  16. */
  17. var VisibilityMixin = {
  18. hidden: false,
  19. show: function() {
  20. this.set("hidden", false);
  21. },
  22. hide: function() {
  23. this.set("hidden", true);
  24. },
  25. toggle: function() {
  26. this.set("hidden", !this.get("hidden"));
  27. },
  28. is_visible: function() {
  29. return !this.attributes.hidden;
  30. }
  31. };
  32. /**
  33. * A tool parameter.
  34. */
  35. var ToolParameter = Backbone.Model.extend({
  36. defaults: {
  37. name: null,
  38. label: null,
  39. type: null,
  40. value: null,
  41. html: null,
  42. num_samples: 5
  43. },
  44. initialize: function(options) {
  45. this.attributes.html = unescape(this.attributes.html);
  46. },
  47. copy: function() {
  48. return new ToolParameter(this.toJSON());
  49. },
  50. set_value: function(value) {
  51. this.set('value', value || '');
  52. }
  53. });
  54. var ToolParameterCollection = Backbone.Collection.extend({
  55. model: ToolParameter
  56. });
  57. /**
  58. * A data tool parameter.
  59. */
  60. var DataToolParameter = ToolParameter.extend({});
  61. /**
  62. * An integer tool parameter.
  63. */
  64. var IntegerToolParameter = ToolParameter.extend({
  65. set_value: function(value) {
  66. this.set('value', parseInt(value, 10));
  67. },
  68. /**
  69. * Returns samples from a tool input.
  70. */
  71. get_samples: function() {
  72. return d3.scale.linear()
  73. .domain([this.get('min'), this.get('max')])
  74. .ticks(this.get('num_samples'));
  75. }
  76. });
  77. var FloatToolParameter = IntegerToolParameter.extend({
  78. set_value: function(value) {
  79. this.set('value', parseFloat(value));
  80. }
  81. });
  82. /**
  83. * A select tool parameter.
  84. */
  85. var SelectToolParameter = ToolParameter.extend({
  86. /**
  87. * Returns tool options.
  88. */
  89. get_samples: function() {
  90. return _.map(this.get('options'), function(option) {
  91. return option[0];
  92. });
  93. }
  94. });
  95. // Set up dictionary of parameter types.
  96. ToolParameter.subModelTypes = {
  97. 'integer': IntegerToolParameter,
  98. 'float': FloatToolParameter,
  99. 'data': DataToolParameter,
  100. 'select': SelectToolParameter
  101. };
  102. /**
  103. * A Galaxy tool.
  104. */
  105. var Tool = Backbone.Model.extend({
  106. // Default attributes.
  107. defaults: {
  108. id: null,
  109. name: null,
  110. description: null,
  111. target: null,
  112. inputs: [],
  113. outputs: []
  114. },
  115. urlRoot: Galaxy.root + 'api/tools',
  116. initialize: function(options) {
  117. // Set parameters.
  118. this.set('inputs', new ToolParameterCollection(_.map(options.inputs, function(p) {
  119. var p_class = ToolParameter.subModelTypes[p.type] || ToolParameter;
  120. return new p_class(p);
  121. })));
  122. },
  123. /**
  124. *
  125. */
  126. toJSON: function() {
  127. var rval = Backbone.Model.prototype.toJSON.call(this);
  128. // Convert inputs to JSON manually.
  129. rval.inputs = this.get('inputs').map(function(i) { return i.toJSON(); });
  130. return rval;
  131. },
  132. /**
  133. * Removes inputs of a particular type; this is useful because not all inputs can be handled by
  134. * client and server yet.
  135. */
  136. remove_inputs: function(types) {
  137. var tool = this,
  138. incompatible_inputs = tool.get('inputs').filter( function(input) {
  139. return ( types.indexOf( input.get('type') ) !== -1);
  140. });
  141. tool.get('inputs').remove(incompatible_inputs);
  142. },
  143. /**
  144. * Returns object copy, optionally including only inputs that can be sampled.
  145. */
  146. copy: function(only_samplable_inputs) {
  147. var copy = new Tool(this.toJSON());
  148. // Return only samplable inputs if flag is set.
  149. if (only_samplable_inputs) {
  150. var valid_inputs = new Backbone.Collection();
  151. copy.get('inputs').each(function(input) {
  152. if (input.get_samples()) {
  153. valid_inputs.push(input);
  154. }
  155. });
  156. copy.set('inputs', valid_inputs);
  157. }
  158. return copy;
  159. },
  160. apply_search_results: function(results) {
  161. ( _.indexOf(results, this.attributes.id) !== -1 ? this.show() : this.hide() );
  162. return this.is_visible();
  163. },
  164. /**
  165. * Set a tool input's value.
  166. */
  167. set_input_value: function(name, value) {
  168. this.get('inputs').find(function(input) {
  169. return input.get('name') === name;
  170. }).set('value', value);
  171. },
  172. /**
  173. * Set many input values at once.
  174. */
  175. set_input_values: function(inputs_dict) {
  176. var self = this;
  177. _.each(_.keys(inputs_dict), function(input_name) {
  178. self.set_input_value(input_name, inputs_dict[input_name]);
  179. });
  180. },
  181. /**
  182. * Run tool; returns a Deferred that resolves to the tool's output(s).
  183. */
  184. run: function() {
  185. return this._run();
  186. },
  187. /**
  188. * Rerun tool using regions and a target dataset.
  189. */
  190. rerun: function(target_dataset, regions) {
  191. return this._run({
  192. action: 'rerun',
  193. target_dataset_id: target_dataset.id,
  194. regions: regions
  195. });
  196. },
  197. /**
  198. * Returns input dict for tool's inputs.
  199. */
  200. get_inputs_dict: function() {
  201. var input_dict = {};
  202. this.get('inputs').each(function(input) {
  203. input_dict[input.get('name')] = input.get('value');
  204. });
  205. return input_dict;
  206. },
  207. /**
  208. * Run tool; returns a Deferred that resolves to the tool's output(s).
  209. * NOTE: this method is a helper method and should not be called directly.
  210. */
  211. _run: function(additional_params) {
  212. // Create payload.
  213. var payload = _.extend({
  214. tool_id: this.id,
  215. inputs: this.get_inputs_dict()
  216. }, additional_params);
  217. // Because job may require indexing datasets, use server-side
  218. // deferred to ensure that job is run. Also use deferred that
  219. // resolves to outputs from tool.
  220. var run_deferred = $.Deferred(),
  221. ss_deferred = new util.ServerStateDeferred({
  222. ajax_settings: {
  223. url: this.urlRoot,
  224. data: JSON.stringify(payload),
  225. dataType: "json",
  226. contentType: 'application/json',
  227. type: "POST"
  228. },
  229. interval: 2000,
  230. success_fn: function(response) {
  231. return response !== "pending";
  232. }
  233. });
  234. // Run job and resolve run_deferred to tool outputs.
  235. $.when(ss_deferred.go()).then(function(result) {
  236. run_deferred.resolve(new data.DatasetCollection(result));
  237. });
  238. return run_deferred;
  239. }
  240. });
  241. _.extend(Tool.prototype, VisibilityMixin);
  242. /**
  243. * Tool view.
  244. */
  245. var ToolView = Backbone.View.extend({
  246. });
  247. /**
  248. * Wrap collection of tools for fast access/manipulation.
  249. */
  250. var ToolCollection = Backbone.Collection.extend({
  251. model: Tool
  252. });
  253. /**
  254. * Label or section header in tool panel.
  255. */
  256. var ToolSectionLabel = Backbone.Model.extend(VisibilityMixin);
  257. /**
  258. * Section of tool panel with elements (labels and tools).
  259. */
  260. var ToolSection = Backbone.Model.extend({
  261. defaults: {
  262. elems: [],
  263. open: false
  264. },
  265. clear_search_results: function() {
  266. _.each(this.attributes.elems, function(elt) {
  267. elt.show();
  268. });
  269. this.show();
  270. this.set("open", false);
  271. },
  272. apply_search_results: function(results) {
  273. var all_hidden = true,
  274. cur_label;
  275. _.each(this.attributes.elems, function(elt) {
  276. if (elt instanceof ToolSectionLabel) {
  277. cur_label = elt;
  278. cur_label.hide();
  279. }
  280. else if (elt instanceof Tool) {
  281. if (elt.apply_search_results(results)) {
  282. all_hidden = false;
  283. if (cur_label) {
  284. cur_label.show();
  285. }
  286. }
  287. }
  288. });
  289. if (all_hidden) {
  290. this.hide();
  291. }
  292. else {
  293. this.show();
  294. this.set("open", true);
  295. }
  296. }
  297. });
  298. _.extend(ToolSection.prototype, VisibilityMixin);
  299. /**
  300. * Tool search that updates results when query is changed. Result value of null
  301. * indicates that query was not run; if not null, results are from search using
  302. * query.
  303. */
  304. var ToolSearch = Backbone.Model.extend({
  305. defaults: {
  306. search_hint_string: "search tools",
  307. min_chars_for_search: 3,
  308. spinner_url: "",
  309. clear_btn_url: "",
  310. search_url: "",
  311. visible: true,
  312. query: "",
  313. results: null,
  314. // ESC (27) will clear the input field and tool search filters
  315. clear_key: 27
  316. },
  317. urlRoot: Galaxy.root + 'api/tools',
  318. initialize: function() {
  319. this.on("change:query", this.do_search);
  320. },
  321. /**
  322. * Do the search and update the results.
  323. */
  324. do_search: function() {
  325. var query = this.attributes.query;
  326. // If query is too short, do not search.
  327. if (query.length < this.attributes.min_chars_for_search) {
  328. this.set("results", null);
  329. return;
  330. }
  331. // Do search via AJAX.
  332. var q = query;
  333. // Stop previous ajax-request
  334. if (this.timer) {
  335. clearTimeout(this.timer);
  336. }
  337. // Start a new ajax-request in X ms
  338. $("#search-clear-btn").hide();
  339. $("#search-spinner").show();
  340. var self = this;
  341. this.timer = setTimeout(function () {
  342. // log the search to analytics if present
  343. if ( typeof ga !== 'undefined' ) {
  344. ga( 'send', 'pageview', Galaxy.root + '?q=' + q );
  345. }
  346. $.get( self.urlRoot, { q: q }, function (data) {
  347. self.set("results", data);
  348. $("#search-spinner").hide();
  349. $("#search-clear-btn").show();
  350. }, "json" );
  351. }, 400 );
  352. },
  353. clear_search: function() {
  354. this.set("query", "");
  355. this.set("results", null);
  356. }
  357. });
  358. _.extend(ToolSearch.prototype, VisibilityMixin);
  359. /**
  360. * Tool Panel.
  361. */
  362. var ToolPanel = Backbone.Model.extend({
  363. initialize: function(options) {
  364. this.attributes.tool_search = options.tool_search;
  365. this.attributes.tool_search.on("change:results", this.apply_search_results, this);
  366. this.attributes.tools = options.tools;
  367. this.attributes.layout = new Backbone.Collection( this.parse(options.layout) );
  368. },
  369. /**
  370. * Parse tool panel dictionary and return collection of tool panel elements.
  371. */
  372. parse: function(response) {
  373. // Recursive function to parse tool panel elements.
  374. var self = this,
  375. // Helper to recursively parse tool panel.
  376. parse_elt = function(elt_dict) {
  377. var type = elt_dict.model_class;
  378. // There are many types of tools; for now, anything that ends in 'Tool'
  379. // is treated as a generic tool.
  380. if ( type.indexOf('Tool') === type.length - 4 ) {
  381. return self.attributes.tools.get(elt_dict.id);
  382. }
  383. else if (type === 'ToolSection') {
  384. // Parse elements.
  385. var elems = _.map(elt_dict.elems, parse_elt);
  386. elt_dict.elems = elems;
  387. return new ToolSection(elt_dict);
  388. }
  389. else if (type === 'ToolSectionLabel') {
  390. return new ToolSectionLabel(elt_dict);
  391. }
  392. };
  393. return _.map(response, parse_elt);
  394. },
  395. clear_search_results: function() {
  396. this.get('layout').each(function(panel_elt) {
  397. if (panel_elt instanceof ToolSection) {
  398. panel_elt.clear_search_results();
  399. }
  400. else {
  401. // Label or tool, so just show.
  402. panel_elt.show();
  403. }
  404. });
  405. },
  406. apply_search_results: function() {
  407. var results = this.get('tool_search').get('results');
  408. if (results === null) {
  409. this.clear_search_results();
  410. return;
  411. }
  412. var cur_label = null;
  413. this.get('layout').each(function(panel_elt) {
  414. if (panel_elt instanceof ToolSectionLabel) {
  415. cur_label = panel_elt;
  416. cur_label.hide();
  417. }
  418. else if (panel_elt instanceof Tool) {
  419. if (panel_elt.apply_search_results(results)) {
  420. if (cur_label) {
  421. cur_label.show();
  422. }
  423. }
  424. }
  425. else {
  426. // Starting new section, so clear current label.
  427. cur_label = null;
  428. panel_elt.apply_search_results(results);
  429. }
  430. });
  431. }
  432. });
  433. /**
  434. * View classes for Galaxy tools and tool panel.
  435. *
  436. * Views use precompiled Handlebars templates for rendering. Views update as needed
  437. * based on (a) model/collection events and (b) user interactions; in this sense,
  438. * they are controllers are well and the HTML is the real view in the MVC architecture.
  439. */
  440. /**
  441. * Base view that handles visibility based on model's hidden attribute.
  442. */
  443. var BaseView = Backbone.View.extend({
  444. initialize: function() {
  445. this.model.on("change:hidden", this.update_visible, this);
  446. this.update_visible();
  447. },
  448. update_visible: function() {
  449. ( this.model.attributes.hidden ? this.$el.hide() : this.$el.show() );
  450. }
  451. });
  452. /**
  453. * Link to a tool.
  454. */
  455. var ToolLinkView = BaseView.extend({
  456. tagName: 'div',
  457. render: function() {
  458. // create element
  459. var $link = $('<div/>');
  460. $link.append(tool_link_template(this.model.toJSON()));
  461. // open upload dialog for upload tool
  462. if (this.model.id === 'upload1') {
  463. $link.find('a').on('click', function(e) {
  464. e.preventDefault();
  465. Galaxy.upload.show();
  466. });
  467. }
  468. else if ( this.model.get( 'model_class' ) === 'Tool' ) { // regular tools
  469. var self = this;
  470. $link.find('a').on('click', function(e) {
  471. e.preventDefault();
  472. var form = new ToolForm.View( { id : self.model.id, version : self.model.get('version') } );
  473. form.deferred.execute(function() {
  474. Galaxy.app.display( form );
  475. });
  476. });
  477. }
  478. // add element
  479. this.$el.append($link);
  480. return this;
  481. }
  482. });
  483. /**
  484. * Panel label/section header.
  485. */
  486. var ToolSectionLabelView = BaseView.extend({
  487. tagName: 'div',
  488. className: 'toolPanelLabel',
  489. render: function() {
  490. this.$el.append( $("<span/>").text(this.model.attributes.text) );
  491. return this;
  492. }
  493. });
  494. /**
  495. * Panel section.
  496. */
  497. var ToolSectionView = BaseView.extend({
  498. tagName: 'div',
  499. className: 'toolSectionWrapper',
  500. initialize: function() {
  501. BaseView.prototype.initialize.call(this);
  502. this.model.on("change:open", this.update_open, this);
  503. },
  504. render: function() {
  505. // Build using template.
  506. this.$el.append( panel_section_template(this.model.toJSON()) );
  507. // Add tools to section.
  508. var section_body = this.$el.find(".toolSectionBody");
  509. _.each(this.model.attributes.elems, function(elt) {
  510. if (elt instanceof Tool) {
  511. var tool_view = new ToolLinkView({model: elt, className: "toolTitle"});
  512. tool_view.render();
  513. section_body.append(tool_view.$el);
  514. }
  515. else if (elt instanceof ToolSectionLabel) {
  516. var label_view = new ToolSectionLabelView({model: elt});
  517. label_view.render();
  518. section_body.append(label_view.$el);
  519. }
  520. else {
  521. // TODO: handle nested section bodies?
  522. }
  523. });
  524. return this;
  525. },
  526. events: {
  527. 'click .toolSectionTitle > a': 'toggle'
  528. },
  529. /**
  530. * Toggle visibility of tool section.
  531. */
  532. toggle: function() {
  533. this.model.set("open", !this.model.attributes.open);
  534. },
  535. /**
  536. * Update whether section is open or close.
  537. */
  538. update_open: function() {
  539. (this.model.attributes.open ?
  540. this.$el.children(".toolSectionBody").slideDown("fast") :
  541. this.$el.children(".toolSectionBody").slideUp("fast")
  542. );
  543. }
  544. });
  545. var ToolSearchView = Backbone.View.extend({
  546. tagName: 'div',
  547. id: 'tool-search',
  548. className: 'bar',
  549. events: {
  550. 'click': 'focus_and_select',
  551. 'keyup :input': 'query_changed',
  552. 'click #search-clear-btn': 'clear'
  553. },
  554. render: function() {
  555. this.$el.append( tool_search_template(this.model.toJSON()) );
  556. if (!this.model.is_visible()) {
  557. this.$el.hide();
  558. }
  559. this.$el.find('[title]').tooltip();
  560. return this;
  561. },
  562. focus_and_select: function() {
  563. this.$el.find(":input").focus().select();
  564. },
  565. clear: function() {
  566. this.model.clear_search();
  567. this.$el.find(":input").val('');
  568. this.focus_and_select();
  569. return false;
  570. },
  571. query_changed: function( evData ) {
  572. // check for the 'clear key' (ESC) first
  573. if( ( this.model.attributes.clear_key ) &&
  574. ( this.model.attributes.clear_key === evData.which ) ){
  575. this.clear();
  576. return false;
  577. }
  578. this.model.set("query", this.$el.find(":input").val());
  579. }
  580. });
  581. /**
  582. * Tool panel view. Events triggered include:
  583. * tool_link_click(click event, tool_model)
  584. */
  585. var ToolPanelView = Backbone.View.extend({
  586. tagName: 'div',
  587. className: 'toolMenu',
  588. /**
  589. * Set up view.
  590. */
  591. initialize: function() {
  592. this.model.get('tool_search').on("change:results", this.handle_search_results, this);
  593. },
  594. render: function() {
  595. var self = this;
  596. // Render search.
  597. var search_view = new ToolSearchView( { model: this.model.get('tool_search') } );
  598. search_view.render();
  599. self.$el.append(search_view.$el);
  600. // Render panel.
  601. this.model.get('layout').each(function(panel_elt) {
  602. if (panel_elt instanceof ToolSection) {
  603. var section_title_view = new ToolSectionView({model: panel_elt});
  604. section_title_view.render();
  605. self.$el.append(section_title_view.$el);
  606. }
  607. else if (panel_elt instanceof Tool) {
  608. var tool_view = new ToolLinkView({model: panel_elt, className: "toolTitleNoSection"});
  609. tool_view.render();
  610. self.$el.append(tool_view.$el);
  611. }
  612. else if (panel_elt instanceof ToolSectionLabel) {
  613. var label_view = new ToolSectionLabelView({model: panel_elt});
  614. label_view.render();
  615. self.$el.append(label_view.$el);
  616. }
  617. });
  618. // Setup tool link click eventing.
  619. self.$el.find("a.tool-link").click(function(e) {
  620. // Tool id is always the first class.
  621. var
  622. tool_id = $(this).attr('class').split(/\s+/)[0],
  623. tool = self.model.get('tools').get(tool_id);
  624. self.trigger("tool_link_click", e, tool);
  625. });
  626. return this;
  627. },
  628. handle_search_results: function() {
  629. var results = this.model.get('tool_search').get('results');
  630. if (results && results.length === 0) {
  631. $("#search-no-results").show();
  632. }
  633. else {
  634. $("#search-no-results").hide();
  635. }
  636. }
  637. });
  638. /**
  639. * View for working with a tool: setting parameters and inputs and executing the tool.
  640. */
  641. var ToolFormView = Backbone.View.extend({
  642. className: 'toolForm',
  643. render: function() {
  644. this.$el.children().remove();
  645. this.$el.append( tool_form_template(this.model.toJSON()) );
  646. }
  647. });
  648. /**
  649. * Integrated tool menu + tool execution.
  650. */
  651. var IntegratedToolMenuAndView = Backbone.View.extend({
  652. className: 'toolMenuAndView',
  653. initialize: function() {
  654. this.tool_panel_view = new ToolPanelView({collection: this.collection});
  655. this.tool_form_view = new ToolFormView();
  656. },
  657. render: function() {
  658. // Render and append tool panel.
  659. this.tool_panel_view.render();
  660. this.tool_panel_view.$el.css("float", "left");
  661. this.$el.append(this.tool_panel_view.$el);
  662. // Append tool form view.
  663. this.tool_form_view.$el.hide();
  664. this.$el.append(this.tool_form_view.$el);
  665. // On tool link click, show tool.
  666. var self = this;
  667. this.tool_panel_view.on("tool_link_click", function(e, tool) {
  668. // Prevents click from activating link:
  669. e.preventDefault();
  670. // Show tool that was clicked on:
  671. self.show_tool(tool);
  672. });
  673. },
  674. /**
  675. * Fetch and display tool.
  676. */
  677. show_tool: function(tool) {
  678. var self = this;
  679. tool.fetch().done( function() {
  680. self.tool_form_view.model = tool;
  681. self.tool_form_view.render();
  682. self.tool_form_view.$el.show();
  683. $('#left').width("650px");
  684. });
  685. }
  686. });
  687. // Exports
  688. return {
  689. ToolParameter: ToolParameter,
  690. IntegerToolParameter: IntegerToolParameter,
  691. SelectToolParameter: SelectToolParameter,
  692. Tool: Tool,
  693. ToolCollection: ToolCollection,
  694. ToolSearch: ToolSearch,
  695. ToolPanel: ToolPanel,
  696. ToolPanelView: ToolPanelView,
  697. ToolFormView: ToolFormView
  698. };
  699. });