10472: Merge branch 'master' into 10472-csummary-cwl-pipeline
[arvados.git] / apps / workbench / app / assets / javascripts / tab_panes.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 // Load tab panes on demand. See app/views/application/_content.html.erb
6
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')).
11         not('.pane-loaded').
12         trigger('arv:pane:reload');
13 });
14
15 // Ask a refreshable pane to reload via ajax.
16 //
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.
20 //
21 // There are four CSS classes set on the element to indicate its state:
22 // pane-loading, pane-stale, pane-loaded, pane-reload-pending
23 //
24 // There are five states based on the presence or absence of css classes:
25 //
26 // 1. Absence of any pane-* states means the pane is empty, and should
27 // be loaded as soon as it becomes visible.
28 //
29 // 2. "pane-loading" means an AJAX call has been made to reload the
30 // pane and we are waiting on a result.
31 //
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
37 // quiescent state).
38 //
39 // 4. "pane-loaded" means the pane is up to date.
40 //
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.
44 //
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.
54         return;
55     }
56
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.
60     var $pane = $(this);
61
62     if ($pane.hasClass('pane-loading')) {
63         // Already loading, mark stale to schedule a reload after this one.
64         $pane.addClass('pane-stale');
65         return;
66     }
67
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.
73     var throttle =
74         $pane.find('.arv-log-refresh-control').attr('data-load-throttle') ||
75         $pane.attr('data-load-throttle') ||
76         15000;
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);
86         }
87         return;
88     }
89
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');
94
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
103         // loading.
104         $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
105         return;
106     }
107
108     $pane.addClass('pane-loading');
109
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) {
113             var $pane = this;
114             // Preserve collapsed state
115             var collapsable = {};
116             $(".collapse", this).each(function(i, c) {
117                 collapsable[c.id] = $(c).hasClass('in');
118             });
119             var tmp = $(data);
120             $(".collapse", tmp).each(function(i, c) {
121                 if (collapsable[c.id]) {
122                     $(c).addClass('in');
123                 } else {
124                     $(c).removeClass('in');
125                 }
126             });
127             $pane.html(tmp);
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]);
132
133             if ($pane.hasClass('pane-stale')) {
134                 $pane.trigger('arv:pane:reload');
135             }
136         }).fail(function(jqxhr, status, error) {
137             var $pane = this;
138             var errhtml;
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');
153                     return;
154                 }
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();
161                 } else {
162                     errhtml = jqxhr.responseText;
163                 }
164             } else {
165                 errhtml = ("An error occurred: " +
166                            (jqxhr.responseText || status)).
167                     replace(/&/g, '&amp;').
168                     replace(/</g, '&lt;').
169                     replace(/>/g, '&gt;');
170             }
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() {
176                 $(this).
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');
181             });
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";
191             }, 1);
192             $pane.removeClass('pane-loading');
193             $pane.addClass('pane-loaded');
194         });
195 });
196
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');
200 });
201
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.
205         return;
206     }
207     // Panes marked arv-refresh-on-log-event should be refreshed
208     $(event.target).trigger('arv:pane:reload');
209 });
210
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 );
216     }
217 });