X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/a77093bdb8cbbba6860bdda67da4d19ad3ac5a0c..e20590d485505f58f7745d74a311ca539c9be940:/apps/workbench/app/assets/javascripts/tab_panes.js diff --git a/apps/workbench/app/assets/javascripts/tab_panes.js b/apps/workbench/app/assets/javascripts/tab_panes.js index cca49b2799..b19a277ef7 100644 --- a/apps/workbench/app/assets/javascripts/tab_panes.js +++ b/apps/workbench/app/assets/javascripts/tab_panes.js @@ -1,85 +1,211 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + // Load tab panes on demand. See app/views/application/_content.html.erb // Fire when a tab is selected/clicked. -$(document).on('shown.bs.tab', '[data-toggle="tab"]', function(e) { - $(this).trigger('arv:pane:reload'); +$(document).on('shown.bs.tab', '[data-toggle="tab"]', function(event) { + // reload the pane (unless it's already loaded) + $($(event.target).attr('href')). + not('.pane-loaded'). + trigger('arv:pane:reload'); }); -// Fire when the content in a pane becomes stale/dirty. If the pane is -// 'active', reload it right away. Otherwise, just replace the current content -// with a spinner for now, don't load the new content unless/until the pane -// becomes active. -$(document).on('arv:pane:reload', function(e) { - // Unload a single pane. Reload it if it's active. - $(e.target).removeClass('loaded'); - var $pane = $($(e.target).attr('href')); - if ($pane.hasClass('active')) { - var content_url = $(e.target).attr('data-pane-content-url'); - $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}). - done(function(data, status, jqxhr) { - // Preserve collapsed state - var collapsable = {}; - $(".collapse", $pane).each(function(i, c) { - collapsable[c.id] = $(c).hasClass('in'); - }); - var tmp = $(data); - $(".collapse", tmp).each(function(i, c) { - if (collapsable[c.id]) { - $(c).addClass('in'); - } else { - $(c).removeClass('in'); - } - }); - $pane.html(tmp); - $(e.target).addClass('loaded'); - $pane.trigger('arv:pane:loaded'); - }).fail(function(jqxhr, status, error) { - var errhtml; - if (jqxhr.getResponseHeader('Content-Type').match(/\btext\/html\b/)) { - var $response = $(jqxhr.responseText); - var $wrapper = $('div#page-wrapper', $response); - if ($wrapper.length) { - errhtml = $wrapper.html(); - } else { - errhtml = jqxhr.responseText; - } +// Ask a refreshable pane to reload via ajax. +// +// Target of this event is the DOM element to be updated. A reload +// consists of an AJAX call to load the "data-pane-content-url" and +// replace the content of the target element with the retrieved HTML. +// +// There are four CSS classes set on the element to indicate its state: +// pane-loading, pane-stale, pane-loaded, pane-reload-pending +// +// There are five states based on the presence or absence of css classes: +// +// 1. Absence of any pane-* states means the pane is empty, and should +// be loaded as soon as it becomes visible. +// +// 2. "pane-loading" means an AJAX call has been made to reload the +// pane and we are waiting on a result. +// +// 3. "pane-loading pane-stale" means the pane is loading, but has +// already been invalidated and should schedule a reload as soon as +// possible after the current load completes. (This happens when there +// is a cluster of events, where the reload is triggered by the first +// event, but we want ensure that we eventually load the final +// quiescent state). +// +// 4. "pane-loaded" means the pane is up to date. +// +// 5. "pane-loaded pane-reload-pending" means a reload is needed, and +// has been scheduled, but has not started because the pane's +// minimum-time-between-reloads throttle has not yet been reached. +// +$(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) { + if (this != e.target) { + // An arv:pane:reload event was sent to an element (e.target) + // which happens to have an ancestor (this) matching the above + // '[data-pane-content-url]' selector. This happens because + // events bubble up the DOM on their way to document. However, + // here we only care about events delivered directly to _this_ + // selected element (i.e., this==e.target), not ones delivered + // to its children. The event "e" is uninteresting here. + return; + } + + // $pane, the event target, is an element whose content is to be + // replaced. Pseudoclasses on $pane (pane-loading, etc) encode the + // current loading state. + var $pane = $(this); + + if ($pane.hasClass('pane-loading')) { + // Already loading, mark stale to schedule a reload after this one. + $pane.addClass('pane-stale'); + return; + } + + // The default throttle (mininum milliseconds between refreshes) + // can be overridden by an .arv-log-refresh-control element inside + // the pane -- or, failing that, the pane element itself -- with a + // data-load-throttle attribute. This allows the server to adjust + // the throttle depending on the pane content. + var throttle = + $pane.find('.arv-log-refresh-control').attr('data-load-throttle') || + $pane.attr('data-load-throttle') || + 15000; + var now = (new Date()).getTime(); + var loaded_at = $pane.attr('data-loaded-at'); + var since_last_load = now - loaded_at; + if (loaded_at && (since_last_load < throttle)) { + if (!$pane.hasClass('pane-reload-pending')) { + $pane.addClass('pane-reload-pending'); + setTimeout((function() { + $pane.trigger('arv:pane:reload'); + }), throttle - since_last_load); + } + return; + } + + // We know this doesn't have 'pane-loading' because we tested for it above + $pane.removeClass('pane-reload-pending'); + $pane.removeClass('pane-loaded'); + $pane.removeClass('pane-stale'); + + if (!$pane.hasClass('active') && + $pane.parent().hasClass('tab-content')) { + // $pane is one of the content areas in a bootstrap tabs + // widget, and it isn't the currently selected tab. If and + // when the user does select the corresponding tab, it will + // get a shown.bs.tab event, which will invoke this reload + // function again (see handler above). For now, we just insert + // a spinner, which will be displayed while the new content is + // loading. + $pane.html('
'); + return; + } + + $pane.addClass('pane-loading'); + + var content_url = $pane.attr('data-pane-content-url'); + $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}). + done(function(data, status, jqxhr) { + var $pane = this; + // Preserve collapsed state + var collapsable = {}; + $(".collapse", this).each(function(i, c) { + collapsable[c.id] = $(c).hasClass('in'); + }); + var tmp = $(data); + $(".collapse", tmp).each(function(i, c) { + if (collapsable[c.id]) { + $(c).addClass('in'); } else { - errhtml = ("An error occurred: " + - (jqxhr.responseText || status)). - replace(/&/g, '&'). - replace(//g, '>'); + $(c).removeClass('in'); } - $pane.html('

' + - '' + - ' ' + - 'Reload tab

'); - $('.tab_reload', $pane).click(function() { - $pane.html('
'); - $(e.target).trigger('arv:pane:reload'); - }); - // We want to render the error in an iframe, in order to - // avoid conflicts with the main page's element ids, etc. - // In order to do that dynamically, we have to set a - // timeout on the iframe window to load our HTML *after* - // the default source (e.g., about:blank) has loaded. - var iframe = $('iframe', e.target)[0]; - iframe.contentWindow.setTimeout(function() { - $('body', iframe.contentDocument).html(errhtml); - iframe.height = iframe.contentDocument.body.scrollHeight + "px"; - }, 1); - $(e.target).addClass('loaded'); }); - } else { - // When the user selects e.target tab, show a spinner instead of - // old content while loading. - $pane.html('
'); - } + $pane.html(tmp); + $pane.removeClass('pane-loading'); + $pane.addClass('pane-loaded'); + $pane.attr('data-loaded-at', (new Date()).getTime()); + $pane.trigger('arv:pane:loaded', [$pane]); + + if ($pane.hasClass('pane-stale')) { + $pane.trigger('arv:pane:reload'); + } + }).fail(function(jqxhr, status, error) { + var $pane = this; + var errhtml; + var contentType = jqxhr.getResponseHeader('Content-Type'); + if (jqxhr.readyState == 0 || jqxhr.status == 0) { + if ($pane.attr('data-loaded-at') > 0) { + // Stale content is already present. Leave it + // there while loading the next page. + $pane.removeClass('pane-loading'); + $pane.addClass('pane-loaded'); + // ...but schedule another refresh (after a + // throttle delay) in case the act of navigating + // away gets cancelled itself, leaving this page + // with content that we know is stale. + $pane.addClass('pane-stale'); + $pane.attr('data-loaded-at', (new Date()).getTime()); + $pane.trigger('arv:pane:reload'); + return; + } + errhtml = "Cancelled."; + } else if (contentType && contentType.match(/\btext\/html\b/)) { + var $response = $(jqxhr.responseText); + var $wrapper = $('div#page-wrapper', $response); + if ($wrapper.length) { + errhtml = $wrapper.html(); + } else { + errhtml = jqxhr.responseText; + } + } else { + errhtml = ("An error occurred: " + + (jqxhr.responseText || status)). + replace(/&/g, '&'). + replace(//g, '>'); + } + $pane.html('

' + + '' + + ' ' + + 'Reload tab

'); + $('.tab_reload', $pane).click(function() { + $(this). + html('
'). + closest('.pane-loaded'). + attr('data-loaded-at', 0). + trigger('arv:pane:reload'); + }); + // We want to render the error in an iframe, in order to + // avoid conflicts with the main page's element ids, etc. + // In order to do that dynamically, we have to set a + // timeout on the iframe window to load our HTML *after* + // the default source (e.g., about:blank) has loaded. + var iframe = $('iframe', $pane)[0]; + iframe.contentWindow.setTimeout(function() { + $('body', iframe.contentDocument).html(errhtml); + iframe.height = iframe.contentDocument.body.scrollHeight + "px"; + }, 1); + $pane.removeClass('pane-loading'); + $pane.addClass('pane-loaded'); + }); }); -// Mark all panes as stale/dirty. Refresh the active pane. -$(document).on('arv-log-event arv:pane:reload:all', function() { - $('.pane-anchor.loaded').trigger('arv:pane:reload'); +// Mark all panes as stale/dirty. Refresh any 'active' panes. +$(document).on('arv:pane:reload:all', function() { + $('[data-pane-content-url]').trigger('arv:pane:reload'); +}); + +$(document).on('arv-log-event', '.arv-refresh-on-log-event', function(event) { + if (this != event.target) { + // Not interested in events sent to child nodes. + return; + } + // Panes marked arv-refresh-on-log-event should be refreshed + $(event.target).trigger('arv:pane:reload'); }); // If there is a 'tab counts url' in the nav-tabs element then use it to get some javascript that will update them