PageRenderTime 79ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/tools.js

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