PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/tools.js

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