|
58 | 58 | justify-content: center; |
59 | 59 | align-items: center; |
60 | 60 | z-index: 9999; |
| 61 | + flex-direction: column; |
| 62 | + gap: 16px; |
| 63 | + } |
| 64 | + #loader.error { |
| 65 | + background: none; |
61 | 66 | } |
62 | 67 | .spinner { |
63 | 68 | width: 50px; |
|
67 | 72 | border-radius: 50%; |
68 | 73 | animation: spin 1s linear infinite; |
69 | 74 | } |
| 75 | + #loader-error { |
| 76 | + display: none; |
| 77 | + color: #d32f2f; |
| 78 | + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| 79 | + font-size: 14px; |
| 80 | + text-align: center; |
| 81 | + padding: 12px 20px; |
| 82 | + background: rgba(211, 47, 47, 0.08); |
| 83 | + border: 1px solid rgba(211, 47, 47, 0.3); |
| 84 | + border-radius: 8px; |
| 85 | + } |
70 | 86 | @keyframes spin { |
71 | 87 | 0% { transform: rotate(0deg); } |
72 | 88 | 100% { transform: rotate(360deg); } |
|
128 | 144 | {% block loader %} |
129 | 145 | <div id="loader" style="display: none;"> |
130 | 146 | <div class="spinner"></div> |
| 147 | + <div id="loader-error"></div> |
131 | 148 | </div> |
132 | 149 | {% endblock %} |
133 | 150 | {% block contents %} |
|
140 | 157 | {% endfor %} |
141 | 158 | {% endfor %} |
142 | 159 | {% endblock contents %} |
| 160 | + <script> |
| 161 | + const _origWebSocket = WebSocket; |
| 162 | + let _wsError = false; |
| 163 | + WebSocket = function(...args) { |
| 164 | + const ws = new _origWebSocket(...args); |
| 165 | + ws.addEventListener('error', function() { |
| 166 | + _wsError = true; |
| 167 | + }); |
| 168 | + ws.addEventListener('close', function(e) { |
| 169 | + if (e.code !== 1000) { |
| 170 | + _wsError = true; |
| 171 | + } |
| 172 | + }); |
| 173 | + return ws; |
| 174 | + }; |
| 175 | + WebSocket.prototype = _origWebSocket.prototype; |
| 176 | + Object.defineProperty(WebSocket, 'CONNECTING', {value: _origWebSocket.CONNECTING}); |
| 177 | + Object.defineProperty(WebSocket, 'OPEN', {value: _origWebSocket.OPEN}); |
| 178 | + Object.defineProperty(WebSocket, 'CLOSING', {value: _origWebSocket.CLOSING}); |
| 179 | + Object.defineProperty(WebSocket, 'CLOSED', {value: _origWebSocket.CLOSED}); |
| 180 | + </script> |
143 | 181 | {{ plot_script | indent(4) }} |
144 | 182 | {% block loader_script %} |
145 | 183 | <script> |
146 | 184 | let timeout = 0; |
| 185 | + |
| 186 | + function showError(message) { |
| 187 | + const loader = document.getElementById('loader'); |
| 188 | + const spinner = loader.querySelector('.spinner'); |
| 189 | + const errorEl = document.getElementById('loader-error'); |
| 190 | + loader.style.display = 'flex'; |
| 191 | + loader.classList.add('error'); |
| 192 | + spinner.style.display = 'none'; |
| 193 | + errorEl.style.display = 'block'; |
| 194 | + errorEl.textContent = message; |
| 195 | + } |
| 196 | + |
147 | 197 | function checkIdle() { |
148 | | - if (timeout > 30000 || (Bokeh.index && Bokeh.index.roots[0] && Bokeh.index.roots[0].is_idle)) { |
| 198 | + if (typeof Bokeh === 'undefined') { |
| 199 | + if (timeout > 10000) { |
| 200 | + showError('Resources could not be loaded.'); |
| 201 | + return; |
| 202 | + } |
| 203 | + } else if (_wsError) { |
| 204 | + showError('Connection with the server could not be established.'); |
| 205 | + return; |
| 206 | + } else if (Bokeh.index && Bokeh.index.roots[0] && Bokeh.index.roots[0].is_idle) { |
| 207 | + document.getElementById('loader').style.display = 'none'; |
| 208 | + return; |
| 209 | + } else if (timeout > 30000) { |
149 | 210 | document.getElementById('loader').style.display = 'none'; |
150 | | - } else { |
151 | | - timeout += 100; |
152 | | - document.getElementById('loader').style.display = 'flex'; |
153 | | - setTimeout(checkIdle, 100); |
| 211 | + return; |
154 | 212 | } |
| 213 | + timeout += 100; |
| 214 | + document.getElementById('loader').style.display = 'flex'; |
| 215 | + setTimeout(checkIdle, 100); |
155 | 216 | } |
156 | 217 | setTimeout(checkIdle, 100) |
157 | 218 | </script> |
|
0 commit comments