4084: Merge branch 'master' into 4084-log-pane-refresh-TC
[arvados.git] / apps / workbench / app / assets / javascripts / tab_panes.js
1 // Load tab panes on demand. See app/views/application/_content.html.erb
2
3 // Fire when a tab is selected/clicked.
4 $(document).on('shown.bs.tab', '[data-toggle="tab"]', function(event) {
5     // reload the pane (unless it's already loaded)
6     $($(event.target).attr('href')).
7         not('.pane-loaded').
8         trigger('arv:pane:reload');
9 });
10
11 // Ask a refreshable pane to reload via ajax.
12 //
13 // Target of this event is the anchor element that manages the pane.  A reload
14 // consists of an AJAX call to load the "data-pane-content-url" and replace the
15 // contents of the DOM node pointed to by "href".
16 //
17 // There are four CSS classes set on the object to indicate its state:
18 // pane-loading, pane-stale, pane-loaded, pane-reload-pending
19 //
20 // There are five states based on the presence or absence of css classes:
21 //
22 // 1. no pane-* states means the pane must be loaded when the pane becomes active
23 //
24 // 2. "pane-loading" means an AJAX call has been made to reload the pane and we are
25 // waiting on a result
26 //
27 // 3. "pane-loading pane-stale" indicates a pane that is already loading has
28 // been invalidated and should schedule a reload immediately when the current
29 // load completes.  (This happens when there is a cluster of events, where the
30 // reload is triggered by the first event, but we want ensure that we
31 // eventually load the final quiescent state).
32 //
33 // 4. "pane-loaded" means the pane is up to date
34 //
35 // 5. "pane-loaded pane-reload-pending" indicates a reload is scheduled (but has
36 // not started yet), suppressing scheduling of any further reloads.
37 //
38 $(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) {
39     // $pane, the event target, is an element whose content is to be
40     // replaced. Pseudoclasses on $pane (pane-loading, etc) encode the
41     // current loading state.
42     var $pane = $(e.target);
43
44     var content_url = $pane.attr('data-pane-content-url');
45     if (!content_url) {
46         // When reloadable elements are nested, we can receive
47         // arv:pane:reload events even though the selector in .on()
48         // does not match e.target. Ignore such events.
49         return;
50     }
51
52     e.stopPropagation();
53
54     if ($pane.hasClass('pane-loading')) {
55         // Already loading, mark stale to schedule a reload after this one.
56         $pane.addClass('pane-stale');
57         return;
58     }
59
60     // The default throttle (mininum milliseconds between refreshes)
61     // can be overridden by an .arv-log-refresh-control element inside
62     // the pane -- or, failing that, the pane element itself -- with a
63     // data-load-throttle attribute. This allows the server to adjust
64     // the throttle depending on the pane content.
65     var throttle =
66         $pane.find('.arv-log-refresh-control').attr('data-load-throttle') ||
67         $pane.attr('data-load-throttle') ||
68         15000;
69     var now = (new Date()).getTime();
70     var loaded_at = $pane.attr('data-loaded-at');
71     var since_last_load = now - loaded_at;
72     if (loaded_at && (since_last_load < throttle)) {
73         if (!$pane.hasClass('pane-reload-pending')) {
74             $pane.addClass('pane-reload-pending');
75             setTimeout((function() {
76                 $pane.trigger('arv:pane:reload');
77             }), throttle - since_last_load);
78         }
79         return;
80     }
81
82     // We know this doesn't have 'pane-loading' because we tested for it above
83     $pane.removeClass('pane-reload-pending');
84     $pane.removeClass('pane-loaded');
85     $pane.removeClass('pane-stale');
86
87     if (!$pane.hasClass('active') &&
88         $pane.parent().hasClass('tab-content')) {
89         // $pane is one of the content areas in a bootstrap tabs
90         // widget, and it isn't the currently selected tab. If and
91         // when the user does select the corresponding tab, it will
92         // get a shown.bs.tab event, which will invoke this reload
93         // function again (see handler above). For now, we just insert
94         // a spinner, which will be displayed while the new content is
95         // loading.
96         $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
97         return;
98     }
99
100     $pane.addClass('pane-loading');
101
102     $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}).
103         done(function(data, status, jqxhr) {
104             // Preserve collapsed state
105             var $pane = this;
106             var collapsable = {};
107             $(".collapse", this).each(function(i, c) {
108                 collapsable[c.id] = $(c).hasClass('in');
109             });
110             var tmp = $(data);
111             $(".collapse", tmp).each(function(i, c) {
112                 if (collapsable[c.id]) {
113                     $(c).addClass('in');
114                 } else {
115                     $(c).removeClass('in');
116                 }
117             });
118             $pane.html(tmp);
119             $pane.removeClass('pane-loading');
120             $pane.addClass('pane-loaded');
121             $pane.attr('data-loaded-at', (new Date()).getTime());
122             $pane.trigger('arv:pane:loaded');
123
124             if ($pane.hasClass('pane-stale')) {
125                 $pane.trigger('arv:pane:reload');
126             }
127         }).fail(function(jqxhr, status, error) {
128             var $pane = this;
129             var errhtml;
130             var contentType = jqxhr.getResponseHeader('Content-Type');
131             if (contentType && contentType.match(/\btext\/html\b/)) {
132                 var $response = $(jqxhr.responseText);
133                 var $wrapper = $('div#page-wrapper', $response);
134                 if ($wrapper.length) {
135                     errhtml = $wrapper.html();
136                 } else {
137                     errhtml = jqxhr.responseText;
138                 }
139             } else {
140                 errhtml = ("An error occurred: " +
141                            (jqxhr.responseText || status)).
142                     replace(/&/g, '&amp;').
143                     replace(/</g, '&lt;').
144                     replace(/>/g, '&gt;');
145             }
146             $pane.html('<div><p>' +
147                       '<a href="#" class="btn btn-primary tab_reload">' +
148                       '<i class="fa fa-fw fa-refresh"></i> ' +
149                       'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
150             $('.tab_reload', $pane).click(function() {
151                 $(this).
152                     html('<div class="spinner spinner-32px spinner-h-center"></div>').
153                     closest('.pane-loaded').
154                     attr('data-loaded-at', 0).
155                     trigger('arv:pane:reload');
156             });
157             // We want to render the error in an iframe, in order to
158             // avoid conflicts with the main page's element ids, etc.
159             // In order to do that dynamically, we have to set a
160             // timeout on the iframe window to load our HTML *after*
161             // the default source (e.g., about:blank) has loaded.
162             var iframe = $('iframe', $pane)[0];
163             iframe.contentWindow.setTimeout(function() {
164                 $('body', iframe.contentDocument).html(errhtml);
165                 iframe.height = iframe.contentDocument.body.scrollHeight + "px";
166             }, 1);
167             $pane.removeClass('pane-loading');
168             $pane.addClass('pane-loaded');
169         });
170 });
171
172 // Mark all panes as stale/dirty. Refresh any 'active' panes.
173 $(document).on('arv:pane:reload:all', function() {
174     $('[data-pane-content-url]').trigger('arv:pane:reload');
175 });
176
177 $(document).on('arv-log-event', '.arv-refresh-on-log-event', function(e) {
178     // Panes marked arv-refresh-on-log-event should be refreshed
179     $(e.target).trigger('arv:pane:reload');
180 });
181
182 // If there is a 'tab counts url' in the nav-tabs element then use it to get some javascript that will update them
183 $(document).on('ready count-change', function() {
184     var tabCountsUrl = $('ul.nav-tabs').data('tab-counts-url');
185     if( tabCountsUrl && tabCountsUrl.length ) {
186         $.get( tabCountsUrl );
187     }
188 });