PageRenderTime 71ms CodeModel.GetById 37ms RepoModel.GetById 0ms app.codeStats 1ms

/static/scripts/mvc/history/readonly-history-panel.js

https://bitbucket.org/hbc/galaxy-central-hbc/
JavaScript | 1122 lines | 705 code | 111 blank | 306 comment | 88 complexity | 83a3b975e497bad9faa032efb785a213 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "mvc/history/history-model",
  3. "mvc/collection/hdca-base",
  4. "mvc/dataset/hda-base",
  5. "mvc/user/user-model",
  6. "mvc/base-mvc",
  7. "utils/localization"
  8. ], function( HISTORY_MODEL, HDCA_BASE, HDA_BASE, USER, BASE_MVC, _l ){
  9. // ============================================================================
  10. /** session storage for individual history preferences */
  11. var HistoryPrefs = BASE_MVC.SessionStorageModel.extend({
  12. defaults : {
  13. //TODO:?? expandedHdas to array?
  14. expandedHdas : {},
  15. //TODO:?? move to user?
  16. show_deleted : false,
  17. show_hidden : false
  18. //TODO: add scroll position?
  19. },
  20. /** add an hda id to the hash of expanded hdas */
  21. addExpandedHda : function( model ){
  22. var key = 'expandedHdas';
  23. this.save( key, _.extend( this.get( key ), _.object([ model.id ], [ model.get( 'id' ) ]) ) );
  24. },
  25. /** remove an hda id from the hash of expanded hdas */
  26. removeExpandedHda : function( id ){
  27. var key = 'expandedHdas';
  28. this.save( key, _.omit( this.get( key ), id ) );
  29. },
  30. toString : function(){
  31. return 'HistoryPrefs(' + this.id + ')';
  32. }
  33. });
  34. // class lvl for access w/o instantiation
  35. HistoryPrefs.storageKeyPrefix = 'history:';
  36. /** key string to store each histories settings under */
  37. HistoryPrefs.historyStorageKey = function historyStorageKey( historyId ){
  38. if( !historyId ){
  39. throw new Error( 'HistoryPrefs.historyStorageKey needs valid id: ' + historyId );
  40. }
  41. // single point of change
  42. return ( HistoryPrefs.storageKeyPrefix + historyId );
  43. };
  44. /** return the existing storage for the history with the given id (or create one if it doesn't exist) */
  45. HistoryPrefs.get = function get( historyId ){
  46. return new HistoryPrefs({ id: HistoryPrefs.historyStorageKey( historyId ) });
  47. };
  48. /** clear all history related items in sessionStorage */
  49. HistoryPrefs.clearAll = function clearAll( historyId ){
  50. for( var key in sessionStorage ){
  51. if( key.indexOf( HistoryPrefs.storageKeyPrefix ) === 0 ){
  52. sessionStorage.removeItem( key );
  53. }
  54. }
  55. };
  56. /* =============================================================================
  57. TODO:
  58. ============================================================================= */
  59. // base model and used as-is in history/view.mako
  60. /** @class non-editable, read-only View/Controller for a history model.
  61. * @name HistoryPanel
  62. *
  63. * Allows:
  64. * changing the loaded history
  65. * searching hdas
  66. * displaying data, info, and download
  67. * Does not allow:
  68. * changing the name
  69. *
  70. * @augments Backbone.View
  71. * @borrows LoggableMixin#logger as #logger
  72. * @borrows LoggableMixin#log as #log
  73. * @constructs
  74. */
  75. var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
  76. /** @lends ReadOnlyHistoryPanel.prototype */{
  77. /** logger used to record this.log messages, commonly set to console */
  78. // comment this out to suppress log output
  79. //logger : console,
  80. /** class to use for constructing the HDA views */
  81. HDAViewClass : HDA_BASE.HDABaseView,
  82. HDCAViewClass : HDCA_BASE.HDCABaseView,
  83. tagName : 'div',
  84. className : 'history-panel',
  85. /** (in ms) that jquery effects will use */
  86. fxSpeed : 'fast',
  87. /** string to display when the model has no hdas */
  88. emptyMsg : _l( 'This history is empty' ),
  89. /** string to no hdas match the search terms */
  90. noneFoundMsg : _l( 'No matching datasets found' ),
  91. // ......................................................................... SET UP
  92. /** Set up the view, set up storage, bind listeners to HistoryContents events
  93. * @param {Object} attributes optional settings for the panel
  94. */
  95. initialize : function( attributes ){
  96. attributes = attributes || {};
  97. // set the logger if requested
  98. if( attributes.logger ){
  99. this.logger = attributes.logger;
  100. }
  101. this.log( this + '.initialize:', attributes );
  102. // ---- instance vars
  103. // control contents/behavior based on where (and in what context) the panel is being used
  104. /** where should pages from links be displayed? (default to new tab/window) */
  105. this.linkTarget = attributes.linkTarget || '_blank';
  106. /** how quickly should jquery fx run? */
  107. this.fxSpeed = _.has( attributes, 'fxSpeed' )?( attributes.fxSpeed ):( this.fxSpeed );
  108. /** filters for displaying hdas */
  109. this.filters = [];
  110. this.searchFor = '';
  111. /** a function to locate the container to scroll to effectively scroll the panel */
  112. //TODO: rename
  113. this.findContainerFn = attributes.findContainerFn;
  114. // generally this is $el.parent() - but may be $el or $el.parent().parent() depending on the context
  115. // ---- sub views and saved elements
  116. /** map of hda model ids to hda views */
  117. this.hdaViews = {};
  118. /** loading indicator */
  119. this.indicator = new LoadingIndicator( this.$el );
  120. // ----- set up panel listeners, handle models passed on init, and call any ready functions
  121. this._setUpListeners();
  122. // don't render when setting the first time
  123. var modelOptions = _.pick( attributes, 'initiallyExpanded', 'show_deleted', 'show_hidden' );
  124. this.setModel( this.model, modelOptions, false );
  125. //TODO: remove?
  126. if( attributes.onready ){
  127. attributes.onready.call( this );
  128. }
  129. },
  130. /** create any event listeners for the panel
  131. * @fires: rendered:initial on the first render
  132. * @fires: empty-history when switching to a history with no HDAs or creating a new history
  133. */
  134. _setUpListeners : function(){
  135. this.on( 'error', function( model, xhr, options, msg, details ){
  136. this.errorHandler( model, xhr, options, msg, details );
  137. });
  138. this.on( 'loading-history', function(){
  139. // show the loading indicator when loading a new history starts...
  140. this._showLoadingIndicator( 'loading history...', 40 );
  141. });
  142. this.on( 'loading-done', function(){
  143. // ...hiding it again when loading is done (or there's been an error)
  144. this._hideLoadingIndicator( 40 );
  145. if( _.isEmpty( this.hdaViews ) ){
  146. this.trigger( 'empty-history', this );
  147. }
  148. });
  149. // throw the first render up as a diff namespace using once (for outside consumption)
  150. this.once( 'rendered', function(){
  151. this.trigger( 'rendered:initial', this );
  152. return false;
  153. });
  154. // debugging
  155. if( this.logger ){
  156. this.on( 'all', function( event ){
  157. this.log( this + '', arguments );
  158. }, this );
  159. }
  160. return this;
  161. },
  162. //TODO: see base-mvc
  163. //onFree : function(){
  164. // _.each( this.hdaViews, function( view, modelId ){
  165. // view.free();
  166. // });
  167. // this.hdaViews = null;
  168. //},
  169. // ........................................................................ error handling
  170. /** Event handler for errors (from the panel, the history, or the history's HDAs)
  171. * @param {Model or View} model the (Backbone) source of the error
  172. * @param {XMLHTTPRequest} xhr any ajax obj. assoc. with the error
  173. * @param {Object} options the options map commonly used with bbone ajax
  174. * @param {String} msg optional message passed to ease error location
  175. * @param {Object} msg optional object containing error details
  176. */
  177. errorHandler : function( model, xhr, options, msg, details ){
  178. console.error( model, xhr, options, msg, details );
  179. //TODO: getting JSON parse errors from jq migrate
  180. // interrupted ajax
  181. if( xhr && xhr.status === 0 && xhr.readyState === 0 ){
  182. // bad gateway
  183. } else if( xhr && xhr.status === 502 ){
  184. //TODO: gmail style 'reconnecting in Ns'
  185. // otherwise, show an error message inside the panel
  186. } else {
  187. var parsed = this._parseErrorMessage( model, xhr, options, msg, details );
  188. // it's possible to have a triggered error before the message container is rendered - wait for it to show
  189. if( !this.$messages().is( ':visible' ) ){
  190. this.once( 'rendered', function(){
  191. this.displayMessage( 'error', parsed.message, parsed.details );
  192. });
  193. } else {
  194. this.displayMessage( 'error', parsed.message, parsed.details );
  195. }
  196. }
  197. },
  198. /** Parse an error event into an Object usable by displayMessage based on the parameters
  199. * note: see errorHandler for more info on params
  200. */
  201. _parseErrorMessage : function( model, xhr, options, msg, details ){
  202. var user = Galaxy.currUser,
  203. // add the args (w/ some extra info) into an obj
  204. parsed = {
  205. message : this._bePolite( msg ),
  206. details : {
  207. user : ( user instanceof USER.User )?( user.toJSON() ):( user + '' ),
  208. source : ( model instanceof Backbone.Model )?( model.toJSON() ):( model + '' ),
  209. xhr : xhr,
  210. options : ( xhr )?( _.omit( options, 'xhr' ) ):( options )
  211. }
  212. };
  213. // add any extra details passed in
  214. _.extend( parsed.details, details || {} );
  215. // fancy xhr.header parsing (--> obj)
  216. if( xhr && _.isFunction( xhr.getAllResponseHeaders ) ){
  217. var responseHeaders = xhr.getAllResponseHeaders();
  218. responseHeaders = _.compact( responseHeaders.split( '\n' ) );
  219. responseHeaders = _.map( responseHeaders, function( header ){
  220. return header.split( ': ' );
  221. });
  222. parsed.details.xhr.responseHeaders = _.object( responseHeaders );
  223. }
  224. return parsed;
  225. },
  226. /** Modify an error message to be fancy and wear a monocle. */
  227. _bePolite : function( msg ){
  228. msg = msg || _l( 'An error occurred while getting updates from the server' );
  229. return msg + '. ' + _l( 'Please contact a Galaxy administrator if the problem persists' ) + '.';
  230. },
  231. // ------------------------------------------------------------------------ loading history/hda models
  232. //NOTE: all the following fns replace the existing history model with a new model
  233. // (in the following 'details' refers to the full set of hda api data (urls, display_apps, misc_info, etc.)
  234. // - hdas w/o details will have summary data only (name, hid, deleted, visible, state, etc.))
  235. /** loads a history & hdas w/ details (but does not make them the current history) */
  236. loadHistoryWithHDADetails : function( historyId, attributes, historyFn, hdaFn ){
  237. //console.info( 'loadHistoryWithHDADetails:', historyId, attributes, historyFn, hdaFn );
  238. var hdaDetailIds = function( historyData ){
  239. // will be called to get hda ids that need details from the api
  240. //TODO: non-visible HDAs are getting details loaded... either stop loading them at all or filter ids thru isVisible
  241. return _.values( HistoryPrefs.get( historyData.id ).get( 'expandedHdas' ) );
  242. };
  243. return this.loadHistory( historyId, attributes, historyFn, hdaFn, hdaDetailIds );
  244. },
  245. /** loads a history & hdas w/ NO details (but does not make them the current history) */
  246. loadHistory : function( historyId, attributes, historyFn, hdaFn, hdaDetailIds ){
  247. var panel = this;
  248. attributes = attributes || {};
  249. panel.trigger( 'loading-history', panel );
  250. //console.info( 'loadHistory:', historyId, attributes, historyFn, hdaFn, hdaDetailIds );
  251. var xhr = HISTORY_MODEL.History.getHistoryData( historyId, {
  252. historyFn : historyFn,
  253. hdaFn : hdaFn,
  254. hdaDetailIds : attributes.initiallyExpanded || hdaDetailIds
  255. });
  256. return panel._loadHistoryFromXHR( xhr, attributes )
  257. .fail( function( xhr, where, history ){
  258. // throw an error up for the error handler
  259. //TODO: difficult to localize - use template
  260. panel.trigger( 'error', panel, xhr, attributes, _l( 'An error was encountered while ' + where ),
  261. { historyId: historyId, history: history || {} });
  262. })
  263. .always( function(){
  264. // bc _hideLoadingIndicator relies on this firing
  265. panel.trigger( 'loading-done', panel );
  266. });
  267. },
  268. /** given an xhr that will provide both history and hda data, pass data to set model or handle xhr errors */
  269. _loadHistoryFromXHR : function( xhr, attributes ){
  270. var panel = this;
  271. xhr.then( function( historyJSON, hdaJSON ){
  272. panel.JSONToModel( historyJSON, hdaJSON, attributes );
  273. });
  274. xhr.fail( function( xhr, where ){
  275. // always render - whether we get a model or not
  276. panel.render();
  277. });
  278. return xhr;
  279. },
  280. /** create a new history model from JSON and call setModel on it */
  281. JSONToModel : function( newHistoryJSON, newHdaJSON, attributes ){
  282. this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON, attributes );
  283. //TODO: Maybe better in History?
  284. attributes = attributes || {};
  285. //this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON.length, attributes );
  286. // // set up the new model and render
  287. // if( Galaxy && Galaxy.currUser ){
  288. ////TODO: global
  289. // newHistoryJSON.user = Galaxy.currUser.toJSON();
  290. // }
  291. var model = new HISTORY_MODEL.History( newHistoryJSON, newHdaJSON, attributes );
  292. this.setModel( model );
  293. return this;
  294. },
  295. /** release/free/shutdown old models and set up panel for new models
  296. * @fires new-model with the panel as parameter
  297. */
  298. setModel : function( model, attributes, render ){
  299. attributes = attributes || {};
  300. render = ( render !== undefined )?( render ):( true );
  301. this.log( 'setModel:', model, attributes, render );
  302. this.freeModel();
  303. this.selectedHdaIds = [];
  304. if( model ){
  305. // set up the new model with user, logger, storage, events
  306. // if( Galaxy && Galaxy.currUser ){
  307. ////TODO: global
  308. // model.user = Galaxy.currUser.toJSON();
  309. // }
  310. this.model = model;
  311. if( this.logger ){
  312. this.model.logger = this.logger;
  313. }
  314. this._setUpWebStorage( attributes.initiallyExpanded, attributes.show_deleted, attributes.show_hidden );
  315. this._setUpModelEventHandlers();
  316. this.trigger( 'new-model', this );
  317. }
  318. if( render ){
  319. //TODO: remove?
  320. this.render();
  321. }
  322. return this;
  323. },
  324. /** free the current model and all listeners for it, free any hdaViews for the model */
  325. freeModel : function(){
  326. // stop/release the previous model, and clear cache to hda sub-views
  327. if( this.model ){
  328. this.model.clearUpdateTimeout();
  329. this.stopListening( this.model );
  330. this.stopListening( this.model.hdas );
  331. //TODO: see base-mvc
  332. //this.model.free();
  333. }
  334. this.freeHdaViews();
  335. return this;
  336. },
  337. /** free any hdaViews the panel has */
  338. freeHdaViews : function(){
  339. this.hdaViews = {};
  340. return this;
  341. },
  342. // ------------------------------------------------------------------------ browser stored prefs
  343. /** Set up client side storage. Currently PersistanStorage keyed under 'HistoryPanel.<id>'
  344. * @param {Object} initiallyExpanded
  345. * @param {Boolean} show_deleted whether to show deleted HDAs (overrides stored)
  346. * @param {Boolean} show_hidden
  347. * @see PersistentStorage
  348. */
  349. _setUpWebStorage : function( initiallyExpanded, show_deleted, show_hidden ){
  350. //this.log( '_setUpWebStorage', initiallyExpanded, show_deleted, show_hidden );
  351. this.storage = new HistoryPrefs({
  352. id: HistoryPrefs.historyStorageKey( this.model.get( 'id' ) )
  353. });
  354. // expanded Hdas is a map of hda.ids -> a boolean repr'ing whether this hda's body is already expanded
  355. // store any pre-expanded ids passed in
  356. if( _.isObject( initiallyExpanded ) ){
  357. this.storage.set( 'exandedHdas', initiallyExpanded );
  358. }
  359. // get the show_deleted/hidden settings giving priority to values passed in, using web storage otherwise
  360. // if the page has specifically requested show_deleted/hidden, these will be either true or false
  361. // (as opposed to undefined, null) - and we give priority to that setting
  362. if( _.isBoolean( show_deleted ) ){
  363. this.storage.set( 'show_deleted', show_deleted );
  364. }
  365. if( _.isBoolean( show_hidden ) ){
  366. this.storage.set( 'show_hidden', show_hidden );
  367. }
  368. this.trigger( 'new-storage', this.storage, this );
  369. this.log( this + ' (init\'d) storage:', this.storage.get() );
  370. return this;
  371. },
  372. // ------------------------------------------------------------------------ history/hda event listening
  373. /** listening for history and HDA events */
  374. _setUpModelEventHandlers : function(){
  375. // ---- hdas
  376. // bind events from the model's hda collection
  377. // note: don't listen to the hdas for errors, history will pass that to us
  378. //this.model.hdas.on( 'reset', this.addAll, this );
  379. this.model.hdas.on( 'add', this.addContentView, this );
  380. // on a model error - bounce it up to the panel and remove it from the model
  381. this.model.on( 'error error:hdas', function( model, xhr, options, msg ){
  382. this.errorHandler( model, xhr, options, msg );
  383. }, this );
  384. return this;
  385. },
  386. // ------------------------------------------------------------------------ panel rendering
  387. /** Render urls, historyPanel body, and hdas (if any are shown)
  388. * @fires: rendered when the panel is attached and fully visible
  389. * @see Backbone.View#render
  390. */
  391. render : function( speed, callback ){
  392. this.log( 'render:', speed, callback );
  393. // send a speed of 0 to have no fade in/out performed
  394. speed = ( speed === undefined )?( this.fxSpeed ):( speed );
  395. var panel = this,
  396. $newRender;
  397. // handle the possibility of no model (can occur if fetching the model returns an error)
  398. if( this.model ){
  399. $newRender = this.renderModel();
  400. } else {
  401. $newRender = this.renderWithoutModel();
  402. }
  403. // fade out existing, swap with the new, fade in, set up behaviours
  404. $( panel ).queue( 'fx', [
  405. function( next ){
  406. if( speed && panel.$el.is( ':visible' ) ){
  407. panel.$el.fadeOut( speed, next );
  408. } else {
  409. next();
  410. }
  411. },
  412. function( next ){
  413. // swap over from temp div newRender
  414. panel.$el.empty();
  415. if( $newRender ){
  416. panel.$el.append( $newRender.children() );
  417. }
  418. next();
  419. },
  420. function( next ){
  421. if( speed && !panel.$el.is( ':visible' ) ){
  422. panel.$el.fadeIn( speed, next );
  423. } else {
  424. next();
  425. }
  426. },
  427. function( next ){
  428. //TODO: ideally, these would be set up before the fade in (can't because of async save text)
  429. if( callback ){ callback.call( this ); }
  430. panel.trigger( 'rendered', this );
  431. next();
  432. }
  433. ]);
  434. return this;
  435. },
  436. /** render without history data
  437. * @returns {jQuery} dom fragment with message container only
  438. */
  439. renderWithoutModel : function( ){
  440. // we'll always need the message container
  441. var $newRender = $( '<div/>' ),
  442. $msgContainer = $( '<div/>' ).addClass( 'message-container' )
  443. .css({ 'margin': '4px' });
  444. return $newRender.append( $msgContainer );
  445. },
  446. /** render with history data
  447. * @returns {jQuery} dom fragment as temporary container to be swapped out later
  448. */
  449. renderModel : function( ){
  450. // tmp div for final swap in render
  451. var $newRender = $( '<div/>' );
  452. // render based on anonymity, set up behaviors
  453. $newRender.append( ReadOnlyHistoryPanel.templates.historyPanel( this.model.toJSON() ) );
  454. this.$emptyMessage( $newRender ).text( this.emptyMsg );
  455. // search and select available to both anon/logged-in users
  456. $newRender.find( '.history-secondary-actions' ).prepend( this._renderSearchButton() );
  457. this._setUpBehaviours( $newRender );
  458. // render hda views (if any and any shown (show_deleted/hidden)
  459. this.renderHdas( $newRender );
  460. return $newRender;
  461. },
  462. /** render the empty/none-found message */
  463. _renderEmptyMsg : function( $whereTo ){
  464. var panel = this,
  465. $emptyMsg = panel.$emptyMessage( $whereTo );
  466. if( !_.isEmpty( panel.hdaViews ) ){
  467. $emptyMsg.hide();
  468. } else if( panel.searchFor ){
  469. $emptyMsg.text( panel.noneFoundMsg ).show();
  470. } else {
  471. $emptyMsg.text( panel.emptyMsg ).show();
  472. }
  473. return this;
  474. },
  475. /** button for opening search */
  476. _renderSearchButton : function( $where ){
  477. return faIconButton({
  478. title : _l( 'Search datasets' ),
  479. classes : 'history-search-btn',
  480. faIcon : 'fa-search'
  481. });
  482. },
  483. /** Set up HistoryPanel js/widget behaviours */
  484. _setUpBehaviours : function( $where ){
  485. //TODO: these should be either sub-MVs, or handled by events
  486. $where = $where || this.$el;
  487. $where.find( '[title]' ).tooltip({ placement: 'bottom' });
  488. this._setUpSearchInput( $where.find( '.history-search-controls .history-search-input' ) );
  489. return this;
  490. },
  491. // ------------------------------------------------------------------------ sub-$element shortcuts
  492. /** the scroll container for this panel - can be $el, $el.parent(), or grandparent depending on context */
  493. $container : function(){
  494. return ( this.findContainerFn )?( this.findContainerFn.call( this ) ):( this.$el.parent() );
  495. },
  496. /** where hdaViews are attached */
  497. $datasetsList : function( $where ){
  498. return ( $where || this.$el ).find( '.datasets-list' );
  499. },
  500. /** container where panel messages are attached */
  501. $messages : function( $where ){
  502. return ( $where || this.$el ).find( '.message-container' );
  503. },
  504. /** the message displayed when no hdaViews can be shown (no hdas, none matching search) */
  505. $emptyMessage : function( $where ){
  506. return ( $where || this.$el ).find( '.empty-history-message' );
  507. },
  508. // ------------------------------------------------------------------------ hda sub-views
  509. /** Set up/render a view for each HDA to be shown, init with model and listeners.
  510. * HDA views are cached to the map this.hdaViews (using the model.id as key).
  511. * @param {jQuery} $whereTo what dom element to prepend the HDA views to
  512. * @returns the number of visible hda views
  513. */
  514. renderHdas : function( $whereTo ){
  515. $whereTo = $whereTo || this.$el;
  516. var panel = this,
  517. newHdaViews = {},
  518. // only render the shown hdas
  519. //TODO: switch to more general filtered pattern
  520. visibleHdas = this.model.hdas.getVisible(
  521. this.storage.get( 'show_deleted' ),
  522. this.storage.get( 'show_hidden' ),
  523. this.filters
  524. );
  525. //this.log( 'renderHdas, visibleHdas:', visibleHdas, $whereTo );
  526. //TODO: prepend to sep div, add as one
  527. this.$datasetsList( $whereTo ).empty();
  528. if( visibleHdas.length ){
  529. visibleHdas.each( function( hda ){
  530. // render it (NOTE: reverse order, newest on top (prepend))
  531. var hdaId = hda.id,
  532. hdaView = panel._createContentView( hda );
  533. newHdaViews[ hdaId ] = hdaView;
  534. // persist selection
  535. if( _.contains( panel.selectedHdaIds, hdaId ) ){
  536. hdaView.selected = true;
  537. }
  538. panel.attachContentView( hdaView.render(), $whereTo );
  539. });
  540. }
  541. this.hdaViews = newHdaViews;
  542. this._renderEmptyMsg( $whereTo );
  543. return this.hdaViews;
  544. },
  545. /** Create an HDA view for the given HDA and set up listeners (but leave attachment for addHdaView)
  546. * @param {HistoryDatasetAssociation} hda
  547. */
  548. _createContentView : function( content ){
  549. var ContentClass = this._getContentClass( content ),
  550. options = _.extend( this._getContentOptions( content ), {
  551. model : content
  552. }),
  553. contentView = new ContentClass( options );
  554. this._setUpHdaListeners( contentView );
  555. return contentView;
  556. },
  557. _getContentClass : function( content ){
  558. var contentType = content.get( "history_content_type" );
  559. switch( contentType ){
  560. case 'dataset':
  561. return this.HDAViewClass;
  562. case 'dataset_collection':
  563. return this.HDCAViewClass;
  564. }
  565. throw new TypeError( 'Unknown history_content_type: ' + contentType );
  566. },
  567. _getContentOptions : function( content ){
  568. return {
  569. linkTarget : this.linkTarget,
  570. expanded : !!this.storage.get( 'expandedHdas' )[ content.id ],
  571. //draggable : true,
  572. hasUser : this.model.ownedByCurrUser(),
  573. logger : this.logger
  574. };
  575. },
  576. /** Set up HistoryPanel listeners for HDAView events. Currently binds:
  577. * HDAView#body-visible, HDAView#body-hidden to store expanded states
  578. * @param {HDAView} hdaView HDAView (base or edit) to listen to
  579. */
  580. _setUpHdaListeners : function( hdaView ){
  581. var panel = this;
  582. hdaView.on( 'error', function( model, xhr, options, msg ){
  583. panel.errorHandler( model, xhr, options, msg );
  584. });
  585. // maintain a list of hdas whose bodies are expanded
  586. hdaView.on( 'expanded', function( model ){
  587. panel.storage.addExpandedHda( model );
  588. });
  589. hdaView.on( 'collapsed', function( id ){
  590. panel.storage.removeExpandedHda( id );
  591. });
  592. return this;
  593. },
  594. /** attach an hdaView to the panel */
  595. attachContentView : function( hdaView, $whereTo ){
  596. $whereTo = $whereTo || this.$el;
  597. var $datasetsList = this.$datasetsList( $whereTo );
  598. $datasetsList.prepend( hdaView.$el );
  599. return this;
  600. },
  601. /** Add an hda view to the panel for the given hda
  602. * @param {HistoryDatasetAssociation} hda
  603. */
  604. addContentView : function( hda ){
  605. this.log( 'add.' + this, hda );
  606. var panel = this;
  607. // don't add the view if it wouldn't be visible accrd. to current settings
  608. if( !hda.isVisible( this.storage.get( 'show_deleted' ), this.storage.get( 'show_hidden' ) ) ){
  609. return panel;
  610. }
  611. // create and prepend to current el, if it was empty fadeout the emptyMsg first
  612. $({}).queue([
  613. function fadeOutEmptyMsg( next ){
  614. var $emptyMsg = panel.$emptyMessage();
  615. if( $emptyMsg.is( ':visible' ) ){
  616. $emptyMsg.fadeOut( panel.fxSpeed, next );
  617. } else {
  618. next();
  619. }
  620. },
  621. function createAndPrepend( next ){
  622. var hdaView = panel._createContentView( hda );
  623. panel.hdaViews[ hda.id ] = hdaView;
  624. hdaView.render().$el.hide();
  625. panel.scrollToTop();
  626. panel.attachContentView( hdaView );
  627. hdaView.$el.slideDown( panel.fxSpeed );
  628. }
  629. ]);
  630. return panel;
  631. },
  632. //TODO: removeHdaView?
  633. /** Set up/render a view for each HDA to be shown, init with model and listeners.
  634. * HDA views are cached to the map this.hdaViews (using the model.id as key).
  635. * @param {jQuery} $whereTo what dom element to prepend the HDA views to
  636. * @returns the number of visible hda views
  637. */
  638. views : function( at ){
  639. var panel = this,
  640. visibleHdas = this.model.hdas.getVisible(
  641. this.storage.get( 'show_deleted' ),
  642. this.storage.get( 'show_hidden' ),
  643. this.filters
  644. );
  645. if( at !== undefined ){
  646. return panel.hdaViews[ visibleHdas.at( at ).id ];
  647. }
  648. return visibleHdas.map( function( hda ){
  649. return panel.hdaViews[ hda.id ];
  650. });
  651. },
  652. /** convenience alias to the model. Updates the hda list only (not the history) */
  653. refreshContents : function( detailIds, options ){
  654. if( this.model ){
  655. return this.model.refresh( detailIds, options );
  656. }
  657. // may have callbacks - so return an empty promise
  658. return $.when();
  659. },
  660. ///** use underscore's findWhere to find a view where the model matches the terms
  661. // * note: finds and returns the _first_ matching
  662. // */
  663. //findHdaView : function( terms ){
  664. // if( !this.model || !this.model.hdas.length ){ return undefined; }
  665. // var model = this.model.hdas.findWhere( terms );
  666. // return ( model )?( this.hdaViews[ model.id ] ):( undefined );
  667. //},
  668. hdaViewRange : function( viewA, viewB ){
  669. //console.debug( 'a: ', viewA, viewA.model );
  670. //console.debug( 'b: ', viewB, viewB.model );
  671. if( viewA === viewB ){ return [ viewA ]; }
  672. //TODO: would probably be better if we cache'd the views as an ordered list (as well as a map)
  673. var panel = this,
  674. withinSet = false,
  675. set = [];
  676. this.model.hdas.getVisible(
  677. this.storage.get( 'show_deleted' ),
  678. this.storage.get( 'show_hidden' ),
  679. this.filters
  680. ).each( function( hda ){
  681. //console.debug( 'checking: ', hda.get( 'name' ) );
  682. if( withinSet ){
  683. //console.debug( '\t\t adding: ', hda.get( 'name' ) );
  684. set.push( panel.hdaViews[ hda.id ] );
  685. if( hda === viewA.model || hda === viewB.model ){
  686. //console.debug( '\t found last: ', hda.get( 'name' ) );
  687. withinSet = false;
  688. }
  689. } else {
  690. if( hda === viewA.model || hda === viewB.model ){
  691. //console.debug( 'found first: ', hda.get( 'name' ) );
  692. withinSet = true;
  693. set.push( panel.hdaViews[ hda.id ] );
  694. }
  695. }
  696. });
  697. return set;
  698. },
  699. // ------------------------------------------------------------------------ panel events
  700. /** event map */
  701. events : {
  702. // allow (error) messages to be clicked away
  703. //TODO: switch to common close (X) idiom
  704. 'click .message-container' : 'clearMessages',
  705. 'click .history-search-btn' : 'toggleSearchControls'
  706. },
  707. /** Collapse all hda bodies and clear expandedHdas in the storage */
  708. collapseAllHdaBodies : function(){
  709. _.each( this.hdaViews, function( item ){
  710. item.collapse();
  711. });
  712. this.storage.set( 'expandedHdas', {} );
  713. return this;
  714. },
  715. /** Handle the user toggling the deleted visibility by:
  716. * (1) storing the new value in the persistent storage
  717. * (2) re-rendering the history
  718. * @returns {Boolean} new show_deleted setting
  719. */
  720. toggleShowDeleted : function( show ){
  721. show = ( show !== undefined )?( show ):( !this.storage.get( 'show_deleted' ) );
  722. this.storage.set( 'show_deleted', show );
  723. this.renderHdas();
  724. return this.storage.get( 'show_deleted' );
  725. },
  726. /** Handle the user toggling the deleted visibility by:
  727. * (1) storing the new value in the persistent storage
  728. * (2) re-rendering the history
  729. * @returns {Boolean} new show_hidden setting
  730. */
  731. toggleShowHidden : function( show ){
  732. show = ( show !== undefined )?( show ):( !this.storage.get( 'show_hidden' ) );
  733. this.storage.set( 'show_hidden', show );
  734. this.renderHdas();
  735. return this.storage.get( 'show_hidden' );
  736. },
  737. // ........................................................................ hda search & filters
  738. /** render a search input for filtering datasets shown
  739. * (see the search section in the HDA model for implementation of the actual searching)
  740. * return will start the search
  741. * esc will clear the search
  742. * clicking the clear button will clear the search
  743. * uses searchInput in ui.js
  744. */
  745. _setUpSearchInput : function( $where ){
  746. var panel = this,
  747. inputSelector = '.history-search-input';
  748. function onFirstSearch( searchFor ){
  749. //this.log( 'onFirstSearch', searchFor, panel );
  750. if( panel.model.hdas.haveDetails() ){
  751. panel.searchHdas( searchFor );
  752. return;
  753. }
  754. panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
  755. panel.model.hdas.fetchAllDetails({ silent: true })
  756. .always( function(){
  757. panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
  758. })
  759. .done( function(){
  760. panel.searchHdas( searchFor );
  761. });
  762. }
  763. $where.searchInput({
  764. initialVal : panel.searchFor,
  765. name : 'history-search',
  766. placeholder : 'search datasets',
  767. classes : 'history-search',
  768. onfirstsearch : onFirstSearch,
  769. onsearch : _.bind( this.searchHdas, this ),
  770. onclear : _.bind( this.clearHdaSearch, this )
  771. });
  772. return $where;
  773. },
  774. /** toggle showing/hiding the search controls (rendering first on the initial show)
  775. * @param {Event or Number} eventOrSpeed variadic - if number the speed of the show/hide effect
  776. * @param {boolean} show force show/hide with T/F
  777. */
  778. toggleSearchControls : function( eventOrSpeed, show ){
  779. var $searchControls = this.$el.find( '.history-search-controls' ),
  780. speed = ( jQuery.type( eventOrSpeed ) === 'number' )?( eventOrSpeed ):( this.fxSpeed );
  781. show = ( show !== undefined )?( show ):( !$searchControls.is( ':visible' ) );
  782. if( show ){
  783. $searchControls.slideDown( speed, function(){
  784. $( this ).find( 'input' ).focus();
  785. });
  786. } else {
  787. $searchControls.slideUp( speed );
  788. }
  789. return show;
  790. },
  791. /** filter hda view list to those that contain the searchFor terms
  792. * (see the search section in the HDA model for implementation of the actual searching)
  793. */
  794. searchHdas : function( searchFor ){
  795. //note: assumes hda details are loaded
  796. //this.log( 'onSearch', searchFor, this );
  797. var panel = this;
  798. this.searchFor = searchFor;
  799. this.filters = [ function( hda ){ return hda.matchesAll( panel.searchFor ); } ];
  800. this.trigger( 'search:searching', searchFor, this );
  801. this.renderHdas();
  802. return this;
  803. },
  804. /** clear the search filters and show all views that are normally shown */
  805. clearHdaSearch : function( searchFor ){
  806. //this.log( 'onSearchClear', this );
  807. this.searchFor = '';
  808. this.filters = [];
  809. this.trigger( 'search:clear', this );
  810. this.renderHdas();
  811. return this;
  812. },
  813. // ........................................................................ loading indicator
  814. /** hide the panel and display a loading indicator (in the panel's parent) when history model's are switched */
  815. _showLoadingIndicator : function( msg, speed, callback ){
  816. speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
  817. if( !this.indicator ){
  818. this.indicator = new LoadingIndicator( this.$el, this.$el.parent() );
  819. }
  820. if( !this.$el.is( ':visible' ) ){
  821. this.indicator.show( 0, callback );
  822. } else {
  823. this.$el.fadeOut( speed );
  824. this.indicator.show( msg, speed, callback );
  825. }
  826. },
  827. /** hide the loading indicator */
  828. _hideLoadingIndicator : function( speed, callback ){
  829. speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
  830. if( this.indicator ){
  831. this.indicator.hide( speed, callback );
  832. }
  833. },
  834. // ........................................................................ (error) messages
  835. /** Display a message in the top of the panel.
  836. * @param {String} type type of message ('done', 'error', 'warning')
  837. * @param {String} msg the message to display
  838. * @param {Object or HTML} modal contents displayed when the user clicks 'details' in the message
  839. */
  840. displayMessage : function( type, msg, details ){
  841. //precondition: msgContainer must have been rendered even if there's no model
  842. var panel = this;
  843. //this.log( 'displayMessage', type, msg, details );
  844. this.scrollToTop();
  845. var $msgContainer = this.$messages(),
  846. $msg = $( '<div/>' ).addClass( type + 'message' ).html( msg );
  847. //this.log( ' ', $msgContainer );
  848. if( !_.isEmpty( details ) ){
  849. var $detailsLink = $( '<a href="javascript:void(0)">Details</a>' )
  850. .click( function(){
  851. Galaxy.modal.show( panel._messageToModalOptions( type, msg, details ) );
  852. return false;
  853. });
  854. $msg.append( ' ', $detailsLink );
  855. }
  856. return $msgContainer.html( $msg );
  857. },
  858. /** convert msg and details into modal options usable by Galaxy.modal */
  859. _messageToModalOptions : function( type, msg, details ){
  860. // only error is fleshed out here
  861. var panel = this,
  862. $modalBody = $( '<div/>' ),
  863. options = { title: 'Details' };
  864. //TODO: to some util library
  865. function objToTable( obj ){
  866. obj = _.omit( obj, _.functions( obj ) );
  867. return [
  868. '<table>',
  869. _.map( obj, function( val, key ){
  870. val = ( _.isObject( val ) )?( objToTable( val ) ):( val );
  871. return '<tr><td style="vertical-align: top; color: grey">' + key + '</td>'
  872. + '<td style="padding-left: 8px">' + val + '</td></tr>';
  873. }).join( '' ),
  874. '</table>'
  875. ].join( '' );
  876. }
  877. if( _.isObject( details ) ){
  878. options.body = $modalBody.append( objToTable( details ) );
  879. } else {
  880. options.body = $modalBody.html( details );
  881. }
  882. options.buttons = {
  883. 'Ok': function(){
  884. Galaxy.modal.hide();
  885. panel.clearMessages();
  886. }
  887. //TODO: if( type === 'error' ){ options.buttons[ 'Report this error' ] = function(){} }
  888. };
  889. return options;
  890. },
  891. /** Remove all messages from the panel.
  892. */
  893. clearMessages : function(){
  894. this.$messages().empty();
  895. return this;
  896. },
  897. // ........................................................................ scrolling
  898. /** get the current scroll position of the panel in its parent */
  899. scrollPosition : function(){
  900. return this.$container().scrollTop();
  901. },
  902. /** set the current scroll position of the panel in its parent */
  903. scrollTo : function( pos ){
  904. this.$container().scrollTop( pos );
  905. return this;
  906. },
  907. /** Scrolls the panel to the top. */
  908. scrollToTop : function(){
  909. this.$container().scrollTop( 0 );
  910. return this;
  911. },
  912. /** Scrolls the panel to show the HDA with the given id.
  913. * @param {String} id the id of HDA to scroll into view
  914. * @returns {HistoryPanel} the panel
  915. */
  916. scrollToId : function( id ){
  917. // do nothing if id not found
  918. if( ( !id ) || ( !this.hdaViews[ id ] ) ){
  919. return this;
  920. }
  921. var view = this.hdaViews[ id ];
  922. //this.scrollIntoView( $viewEl.offset().top );
  923. this.scrollTo( view.el.offsetTop );
  924. return this;
  925. },
  926. /** Scrolls the panel to show the HDA with the given hid.
  927. * @param {Integer} hid the hid of HDA to scroll into view
  928. * @returns {HistoryPanel} the panel
  929. */
  930. scrollToHid : function( hid ){
  931. var hda = this.model.hdas.getByHid( hid );
  932. // do nothing if hid not found
  933. if( !hda ){ return this; }
  934. return this.scrollToId( hda.id );
  935. },
  936. // ........................................................................ misc
  937. print : function(){
  938. var panel = this;
  939. panel.debug( this );
  940. _.each( this.hdaViews, function( view, id ){
  941. panel.debug( '\t ' + id, view );
  942. });
  943. },
  944. /** Return a string rep of the history */
  945. toString : function(){
  946. return 'ReadOnlyHistoryPanel(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
  947. }
  948. });
  949. //------------------------------------------------------------------------------ TEMPLATES
  950. var _panelTemplate = [
  951. '<div class="history-controls">',
  952. '<div class="history-search-controls">',
  953. '<div class="history-search-input"></div>',
  954. '</div>',
  955. '<div class="history-title">',
  956. '<% if( history.name ){ %>',
  957. '<div class="history-name"><%= history.name %></div>',
  958. '<% } %>',
  959. '</div>',
  960. '<div class="history-subtitle clear">',
  961. '<% if( history.nice_size ){ %>',
  962. '<div class="history-size"><%= history.nice_size %></div>',
  963. '<% } %>',
  964. '<div class="history-secondary-actions"></div>',
  965. '</div>',
  966. '<% if( history.deleted ){ %>',
  967. '<div class="warningmessagesmall"><strong>',
  968. _l( 'You are currently viewing a deleted history!' ),
  969. '</strong></div>',
  970. '<% } %>',
  971. '<div class="message-container">',
  972. '<% if( history.message ){ %>',
  973. // should already be localized
  974. '<div class="<%= history.status %>message"><%= history.message %></div>',
  975. '<% } %>',
  976. '</div>',
  977. '<div class="quota-message errormessage">',
  978. _l( 'You are over your disk quota' ), '. ',
  979. _l( 'Tool execution is on hold until your disk usage drops below your allocated quota' ), '.',
  980. '</div>',
  981. '<div class="tags-display"></div>',
  982. '<div class="annotation-display"></div>',
  983. '<div class="history-dataset-actions">',
  984. '<div class="btn-group">',
  985. '<button class="history-select-all-datasets-btn btn btn-default"',
  986. 'data-mode="select">', _l( 'All' ), '</button>',
  987. '<button class="history-deselect-all-datasets-btn btn btn-default"',
  988. 'data-mode="select">', _l( 'None' ), '</button>',
  989. '</div>',
  990. '<button class="history-dataset-action-popup-btn btn btn-default">',
  991. _l( 'For all selected' ), '...</button>',
  992. '</div>',
  993. '</div>',
  994. // end history controls
  995. // where the datasets/hdas are added
  996. '<div class="datasets-list"></div>',
  997. '<div class="empty-history-message infomessagesmall">',
  998. _l( 'This history is empty' ),
  999. '</div>'
  1000. ].join( '' );
  1001. ReadOnlyHistoryPanel.templates = {
  1002. historyPanel : function( historyJSON ){
  1003. return _.template( _panelTemplate, historyJSON, { variable: 'history' });
  1004. }
  1005. };
  1006. //==============================================================================
  1007. return {
  1008. ReadOnlyHistoryPanel: ReadOnlyHistoryPanel
  1009. };
  1010. });