PageRenderTime 58ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/tools.js

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