1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 // Load tab panes on demand. See app/views/application/_content.html.erb
7 // Fire when a tab is selected/clicked.
8 $(document).on('shown.bs.tab', '[data-toggle="tab"]', function(event) {
9 // reload the pane (unless it's already loaded)
10 $($(event.target).attr('href')).
12 trigger('arv:pane:reload');
15 // Ask a refreshable pane to reload via ajax.
17 // Target of this event is the DOM element to be updated. A reload
18 // consists of an AJAX call to load the "data-pane-content-url" and
19 // replace the content of the target element with the retrieved HTML.
21 // There are four CSS classes set on the element to indicate its state:
22 // pane-loading, pane-stale, pane-loaded, pane-reload-pending
24 // There are five states based on the presence or absence of css classes:
26 // 1. Absence of any pane-* states means the pane is empty, and should
27 // be loaded as soon as it becomes visible.
29 // 2. "pane-loading" means an AJAX call has been made to reload the
30 // pane and we are waiting on a result.
32 // 3. "pane-loading pane-stale" means the pane is loading, but has
33 // already been invalidated and should schedule a reload as soon as
34 // possible after the current load completes. (This happens when there
35 // is a cluster of events, where the reload is triggered by the first
36 // event, but we want ensure that we eventually load the final
39 // 4. "pane-loaded" means the pane is up to date.
41 // 5. "pane-loaded pane-reload-pending" means a reload is needed, and
42 // has been scheduled, but has not started because the pane's
43 // minimum-time-between-reloads throttle has not yet been reached.
45 $(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) {
46 if (this != e.target) {
47 // An arv:pane:reload event was sent to an element (e.target)
48 // which happens to have an ancestor (this) matching the above
49 // '[data-pane-content-url]' selector. This happens because
50 // events bubble up the DOM on their way to document. However,
51 // here we only care about events delivered directly to _this_
52 // selected element (i.e., this==e.target), not ones delivered
53 // to its children. The event "e" is uninteresting here.
57 // $pane, the event target, is an element whose content is to be
58 // replaced. Pseudoclasses on $pane (pane-loading, etc) encode the
59 // current loading state.
62 if ($pane.hasClass('pane-loading')) {
63 // Already loading, mark stale to schedule a reload after this one.
64 $pane.addClass('pane-stale');
68 // The default throttle (mininum milliseconds between refreshes)
69 // can be overridden by an .arv-log-refresh-control element inside
70 // the pane -- or, failing that, the pane element itself -- with a
71 // data-load-throttle attribute. This allows the server to adjust
72 // the throttle depending on the pane content.
74 $pane.find('.arv-log-refresh-control').attr('data-load-throttle') ||
75 $pane.attr('data-load-throttle') ||
77 var now = (new Date()).getTime();
78 var loaded_at = $pane.attr('data-loaded-at');
79 var since_last_load = now - loaded_at;
80 if (loaded_at && (since_last_load < throttle)) {
81 if (!$pane.hasClass('pane-reload-pending')) {
82 $pane.addClass('pane-reload-pending');
83 setTimeout((function() {
84 $pane.trigger('arv:pane:reload');
85 }), throttle - since_last_load);
90 // We know this doesn't have 'pane-loading' because we tested for it above
91 $pane.removeClass('pane-reload-pending');
92 $pane.removeClass('pane-loaded');
93 $pane.removeClass('pane-stale');
95 if (!$pane.hasClass('active') &&
96 $pane.parent().hasClass('tab-content')) {
97 // $pane is one of the content areas in a bootstrap tabs
98 // widget, and it isn't the currently selected tab. If and
99 // when the user does select the corresponding tab, it will
100 // get a shown.bs.tab event, which will invoke this reload
101 // function again (see handler above). For now, we just insert
102 // a spinner, which will be displayed while the new content is
104 $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
108 $pane.addClass('pane-loading');
110 var content_url = $pane.attr('data-pane-content-url');
111 $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}).
112 done(function(data, status, jqxhr) {
114 // Preserve collapsed state
115 var collapsable = {};
116 $(".collapse", this).each(function(i, c) {
117 collapsable[c.id] = $(c).hasClass('in');
120 $(".collapse", tmp).each(function(i, c) {
121 if (collapsable[c.id]) {
124 $(c).removeClass('in');
128 $pane.removeClass('pane-loading');
129 $pane.addClass('pane-loaded');
130 $pane.attr('data-loaded-at', (new Date()).getTime());
131 $pane.trigger('arv:pane:loaded', [$pane]);
133 if ($pane.hasClass('pane-stale')) {
134 $pane.trigger('arv:pane:reload');
136 }).fail(function(jqxhr, status, error) {
139 var contentType = jqxhr.getResponseHeader('Content-Type');
140 if (jqxhr.readyState == 0 || jqxhr.status == 0) {
141 if ($pane.attr('data-loaded-at') > 0) {
142 // Stale content is already present. Leave it
143 // there while loading the next page.
144 $pane.removeClass('pane-loading');
145 $pane.addClass('pane-loaded');
146 // ...but schedule another refresh (after a
147 // throttle delay) in case the act of navigating
148 // away gets cancelled itself, leaving this page
149 // with content that we know is stale.
150 $pane.addClass('pane-stale');
151 $pane.attr('data-loaded-at', (new Date()).getTime());
152 $pane.trigger('arv:pane:reload');
155 errhtml = "Cancelled.";
156 } else if (contentType && contentType.match(/\btext\/html\b/)) {
157 var $response = $(jqxhr.responseText);
158 var $wrapper = $('div#page-wrapper', $response);
159 if ($wrapper.length) {
160 errhtml = $wrapper.html();
162 errhtml = jqxhr.responseText;
165 errhtml = ("An error occurred: " +
166 (jqxhr.responseText || status)).
167 replace(/&/g, '&').
168 replace(/</g, '<').
169 replace(/>/g, '>');
171 $pane.html('<div class="pane-error-display"><p>' +
172 '<a href="#" class="btn btn-primary tab_reload">' +
173 '<i class="fa fa-fw fa-refresh"></i> ' +
174 'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
175 $('.tab_reload', $pane).click(function() {
177 html('<div class="spinner spinner-32px spinner-h-center"></div>').
178 closest('.pane-loaded').
179 attr('data-loaded-at', 0).
180 trigger('arv:pane:reload');
182 // We want to render the error in an iframe, in order to
183 // avoid conflicts with the main page's element ids, etc.
184 // In order to do that dynamically, we have to set a
185 // timeout on the iframe window to load our HTML *after*
186 // the default source (e.g., about:blank) has loaded.
187 var iframe = $('iframe', $pane)[0];
188 iframe.contentWindow.setTimeout(function() {
189 $('body', iframe.contentDocument).html(errhtml);
190 iframe.height = iframe.contentDocument.body.scrollHeight + "px";
192 $pane.removeClass('pane-loading');
193 $pane.addClass('pane-loaded');
197 // Mark all panes as stale/dirty. Refresh any 'active' panes.
198 $(document).on('arv:pane:reload:all', function() {
199 $('[data-pane-content-url]').trigger('arv:pane:reload');
202 $(document).on('arv-log-event', '.arv-refresh-on-log-event', function(event) {
203 if (this != event.target) {
204 // Not interested in events sent to child nodes.
207 // Panes marked arv-refresh-on-log-event should be refreshed
208 $(event.target).trigger('arv:pane:reload');
211 // If there is a 'tab counts url' in the nav-tabs element then use it to get some javascript that will update them
212 $(document).on('ready count-change', function() {
213 var tabCountsUrl = $('ul.nav-tabs').data('tab-counts-url');
214 if( tabCountsUrl && tabCountsUrl.length ) {
215 $.get( tabCountsUrl );