PageRenderTime 45ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/static/scripts/mvc/tools.js

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