Fix 2.4.2 upgrade notes formatting refs #19330
[arvados.git] / apps / workbench / app / assets / javascripts / tab_panes.js
index 0430ca9e3e9e6958b8097117c70e766e960000b1..b19a277ef76478f75ca73538a793eae6add0f856 100644 (file)
+// 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(event) {
-    // When we switch tabs, remove "active" from any refreshable panes within
-    // the previous tab content so they don't continue to refresh unnecessarily, and
-    // add "active" to any refreshable panes under the newly shown tab content.
-
-    var tgt = $($(event.relatedTarget).attr('href'));
-    $(".pane-anchor", tgt).each(function (i, e) {
-        var a = $($(e).attr('href'));
-        a.removeClass("active");
-    });
-
-    tgt = $($(event.target).attr('href'));
-    $(".pane-anchor", tgt).each(function (i, e) {
-        var a = $($(e).attr('href'));
-        a.addClass("active");
-    });
-
-    // Now trigger reload of the newly shown tab pane.
-    $(event.target).trigger('arv:pane:reload');
+    // reload the pane (unless it's already loaded)
+    $($(event.target).attr('href')).
+        not('.pane-loaded').
+        trigger('arv:pane:reload');
 });
 
 // Ask a refreshable pane to reload via ajax.
-// Target of this event is the anchoring element that manages the pane.
 //
-// Panes can be in one of three primary states, managed by setting CSS classes
-// on the object: not loaded (no pane-* state classes), pane-loading, pane-loaded
+// 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.
 //
-// not loaded means the pane needs to be loaded when the pane becomes active
+// There are four CSS classes set on the element to indicate its state:
+// pane-loading, pane-stale, pane-loaded, pane-reload-pending
 //
-// pane-loading means there is a current AJAX call outstanding to reload the pane
+// There are five states based on the presence or absence of css classes:
 //
-// pane-loaded means the pane is believe to be up to date
+// 1. Absence of any pane-* states means the pane is empty, and should
+// be loaded as soon as it becomes visible.
 //
-// There are two additional states: pane-stale and pane-reload-pending
+// 2. "pane-loading" means an AJAX call has been made to reload the
+// pane and we are waiting on a result.
 //
-// pane-stale indicates a pane that is already loading has been invalidated and
-// should schedule a reload immediately when the current load completes.  (This
-// happens if there are clusters of events, where the reload is trigged by the
-// first event, but we actually want to display the state after the final event
-// has been processed.)
+// 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).
 //
-// pane-reload-pending indicates a reload is scheduled, to suppress
-// scheduling any additional reloads.
-$(document).on('arv:pane:reload', function(e) {
-    e.stopPropagation();
-
-    // '$anchor' is the event target, which is a .pane-anchor or a bootstrap
-    // tab anchor.  This is the element that stores the state of the pane.  The
-    // actual element that will contain the content is pointed to in the 'href'
-    // attribute of etarget.
-    var $anchor = $(e.target);
-
-    if ($anchor.hasClass('pane-loading')) {
-        // Already loading, mark stale to schedule a reload after this one.
-        $anchor.addClass('pane-stale');
+// 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;
     }
 
-    if ($anchor.hasClass('pane-no-auto-reload') && $anchor.hasClass('pane-loaded')) {
-        // Have to explicitly remove pane-loaded if we want it to reload.
+    // $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;
     }
 
-    var throttle = $anchor.attr('data-load-throttle');
-    if (!throttle) {
-        throttle = 3000;
-    }
+    // 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 = $anchor.attr('data-loaded-at');
+    var loaded_at = $pane.attr('data-loaded-at');
     var since_last_load = now - loaded_at;
     if (loaded_at && (since_last_load < throttle)) {
-        if (!$anchor.hasClass('pane-reload-pending')) {
-            $anchor.addClass('pane-reload-pending');
+        if (!$pane.hasClass('pane-reload-pending')) {
+            $pane.addClass('pane-reload-pending');
             setTimeout((function() {
-                $anchor.trigger('arv:pane:reload');
+                $pane.trigger('arv:pane:reload');
             }), throttle - since_last_load);
         }
         return;
     }
 
     // We know this doesn't have 'pane-loading' because we tested for it above
-    $anchor.removeClass('pane-reload-pending');
-    $anchor.removeClass('pane-loaded');
-    $anchor.removeClass('pane-stale');
-
-    // $pane is the actual content area that is going to be updated.
-    var $pane = $($anchor.attr('href'));
-    if ($pane.hasClass('active')) {
-        $anchor.addClass('pane-loading');
+    $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('<div class="spinner spinner-32px spinner-h-center"></div>');
+        return;
+    }
 
-        var content_url = $anchor.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", $(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 {
-                        $(c).removeClass('in');
-                    }
-                });
-                $(this).html(tmp);
-                $anchor.removeClass('pane-loading');
-                $anchor.addClass('pane-loaded');
-                $anchor.attr('data-loaded-at', (new Date()).getTime());
-                $(this).trigger('arv:pane:loaded');
+    $pane.addClass('pane-loading');
 
-                if ($anchor.hasClass('pane-stale')) {
-                    $anchor.trigger('arv:pane:reload');
+    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 {
+                    $(c).removeClass('in');
                 }
-            }).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;
-                    }
+            });
+            $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 = ("An error occurred: " +
-                               (jqxhr.responseText || status)).
-                        replace(/&/g, '&amp;').
-                        replace(/</g, '&lt;').
-                        replace(/>/g, '&gt;');
+                    errhtml = jqxhr.responseText;
                 }
-                $(this).html('<div><p>' +
-                        '<a href="#" class="btn btn-primary tab_reload">' +
-                        '<i class="fa fa-fw fa-refresh"></i> ' +
-                        'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
-                $('.tab_reload', $(this)).click(function() {
-                    $(this).html('<div class="spinner spinner-32px spinner-h-center"></div>');
-                    $anchor.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', $(this))[0];
-                iframe.contentWindow.setTimeout(function() {
-                    $('body', iframe.contentDocument).html(errhtml);
-                    iframe.height = iframe.contentDocument.body.scrollHeight + "px";
-                }, 1);
-                $anchor.addClass('pane-loaded');
+            } else {
+                errhtml = ("An error occurred: " +
+                           (jqxhr.responseText || status)).
+                    replace(/&/g, '&amp;').
+                    replace(/</g, '&lt;').
+                    replace(/>/g, '&gt;');
+            }
+            $pane.html('<div class="pane-error-display"><p>' +
+                      '<a href="#" class="btn btn-primary tab_reload">' +
+                      '<i class="fa fa-fw fa-refresh"></i> ' +
+                      'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
+            $('.tab_reload', $pane).click(function() {
+                $(this).
+                    html('<div class="spinner spinner-32px spinner-h-center"></div>').
+                    closest('.pane-loaded').
+                    attr('data-loaded-at', 0).
+                    trigger('arv:pane:reload');
             });
-    } else {
-        // When the user selects e.target tab, show a spinner instead of
-        // old content while loading.
-        $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
-    }
+            // 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 any 'active' panes.
 $(document).on('arv:pane:reload:all', function() {
-    $('.pane-anchor').trigger('arv:pane:reload');
+    $('[data-pane-content-url]').trigger('arv:pane:reload');
 });
 
-$(document).on('ready ajax:complete', function() {
+$(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
-    $('.pane-anchor.arv-refresh-on-log-event').on('arv-log-event', function(e) {
-        $(e.target).trigger('arv:pane:reload');
-    });
+    $(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