Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 <!-- | 1 <!-- |
| 2 Copyright 2016 The LUCI Authors. All rights reserved. | 2 Copyright 2016 The LUCI Authors. All rights reserved. |
| 3 Use of this source code is governed under the Apache License, Version 2.0 | 3 Use of this source code is governed under the Apache License, Version 2.0 |
| 4 that can be found in the LICENSE file. | 4 that can be found in the LICENSE file. |
| 5 --> | 5 --> |
| 6 | 6 |
| 7 <link rel="import" href="../bower_components/polymer/polymer.html"> | 7 <link rel="import" href="../bower_components/polymer/polymer.html"> |
| 8 <link rel="import" href="../bower_components/google-signin/google-signin-aware.h tml"> | 8 <link rel="import" href="../bower_components/google-signin/google-signin-aware.h tml"> |
| 9 <link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html" > | 9 <link rel="import" href="../bower_components/iron-icons/iron-icons.html"> |
| 10 | 10 <link rel="import" href="../bower_components/iron-icons/av-icons.html"> |
| 11 <link rel="import" href="../logdog-stream/logdog-stream.html"> | 11 <link rel="import" href="../bower_components/iron-icons/editor-icons.html"> |
| 12 <link rel="import" href="../logdog-stream/logdog-error.html"> | 12 <link rel="import" href="../bower_components/paper-button/paper-button.html"> |
| 13 <link rel="import" href="../luci-sleep-promise/luci-sleep-promise.html"> | 13 <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button .html"> |
| 14 <link rel="import" href="logdog-stream-fetcher.html"> | 14 <link rel="import" href=""> |
| 15 <link rel="import" href="logdog-stream-query.html"> | |
| 16 | 15 |
| 17 <!-- | 16 <!-- |
| 18 An element for rendering muxed LogDog log streams. | 17 An element for rendering muxed LogDog log streams. |
| 19 --> | 18 --> |
| 20 <dom-module id="logdog-stream-view"> | 19 <dom-module id="logdog-stream-view"> |
| 21 | 20 |
| 22 <template> | 21 <template> |
| 23 <style> | 22 <style is="custom-style"> |
| 24 .buttons { | 23 #mainView { |
| 24 position: relative; | |
| 25 } | |
| 26 | |
| 27 #buttons { | |
| 28 position: fixed; | |
| 29 height: auto; | |
| 30 padding: 5px; | |
| 31 background-color: rgba(0, 0, 0, 0.1); | |
| 32 z-index: 100; | |
| 33 } | |
| 34 #buttons > paper-button { | |
| 25 background-color: white; | 35 background-color: white; |
| 26 } | 36 } |
| 27 | 37 |
| 28 #stream-status { | 38 .paper-button-highlight[toggles][active] { |
| 39 background-color: #cd6a51; | |
| 40 } | |
| 41 | |
| 42 .paper-icon-button-highlight[toggles][active] { | |
| 43 background-color: #cd6a51; | |
| 44 border-radius: 80%; | |
| 45 } | |
| 46 | |
| 47 #streamStatus { | |
| 29 position: fixed; | 48 position: fixed; |
| 30 right: 16px; | 49 right: 16px; |
| 31 background-color: #EEEEEE; | 50 background-color: #EEEEEE; |
| 32 opacity: 0.7; | 51 opacity: 0.7; |
| 33 } | 52 } |
| 34 | 53 |
| 35 #logContent { | 54 #logContent { |
| 36 padding-top: 20px; | 55 padding-top: 54px; /* Pad around buttons */ |
| 37 background-color: white; | 56 background-color: white; |
| 38 } | 57 } |
| 39 | 58 |
| 40 .log-entry { | 59 .log-entry { |
| 41 padding: 0 0 0 0; | 60 padding: 0 0 0 0; |
| 42 clear: left; | 61 clear: left; |
| 43 } | 62 } |
| 44 | 63 |
| 45 .log-entry-meta { | 64 .log-entry-meta { |
| 46 vertical-align: top; | 65 background-color: lightgray; |
| 47 padding: 0 8px 0 0; | 66 padding: 5px; |
| 48 margin: 0 0 0 0; | 67 border-width: 2px 0px 0px 0px; |
| 49 float: left; | 68 border-color: darkgray; |
| 69 border-style: dotted; | |
| 70 user-select: none; | |
| 71 | |
| 50 font-style: italic; | 72 font-style: italic; |
| 51 font-family: Courier New, Courier, monospace; | 73 font-family: Courier New, Courier, monospace; |
| 52 font-size: 10px; | 74 font-size: 10px; |
| 53 | 75 |
| 54 /* Fixed width, break word if necessary. */ | 76 /* Can be toggled to "flex" by applying .showMeta class to #logs. */ |
| 55 width: 150px; | |
| 56 word-break: break-word; | |
| 57 | |
| 58 /* Can be toggled by applying .showMeta class to #logs. */ | |
| 59 display: none; | 77 display: none; |
| 60 } | 78 } |
| 79 .log-entry-meta-line { | |
| 80 padding: 5px; | |
| 81 border-width: 1px; | |
| 82 border-style: solid; | |
| 83 border-color: gray; | |
| 84 border-radius: 10px; | |
| 85 margin-right: 10px; | |
| 86 text-align: center; | |
| 87 } | |
| 61 .showMeta .log-entry-meta { | 88 .showMeta .log-entry-meta { |
| 62 display: block; | 89 display: flex; |
| 63 } | 90 } |
| 64 | 91 |
| 65 .log-entry-content { | 92 /* .log-entry-content { */ |
| 93 .log-entry-chunk { | |
| 66 padding: 0 0 0 0; | 94 padding: 0 0 0 0; |
| 67 margin: 0 0 0 0; | 95 margin: 0 0 0 0; |
| 68 float: none; | 96 float: none; |
| 69 font-family: Courier New, Courier, monospace; | 97 font-family: Courier New, Courier, monospace; |
| 70 font-size: 16px; | 98 font-size: 16px; |
| 71 list-style: none; | 99 list-style: none; |
| 72 } | |
| 73 | 100 |
| 74 .log-entry-line { | 101 border-bottom: 1px solid #CCCCCC; |
| 75 padding-left: 0; | |
| 76 | 102 |
| 77 /* Can be toggled by applying .wrapLines class to #logs. */ | 103 /* Can be toggled by applying .wrapLines class to #logs. */ |
| 78 white-space: pre; | 104 white-space: pre; |
| 79 } | 105 } |
| 80 .wrapLines .log-entry-line { | 106 |
| 107 /*.wrapLines .log-entry-content { */ | |
| 108 .wrapLines .log-entry-chunk { | |
| 81 white-space: pre-wrap; | 109 white-space: pre-wrap; |
| 82 word-break: break-word; | 110 word-break: break-word; |
| 83 } | 111 } |
| 84 | 112 |
| 85 .log-entry-line:nth-last-child(2) { | 113 .logFetchButtonContainer { |
| 86 border-bottom: 1px solid #CCCCCC; | 114 height: auto; |
| 115 display: none; | |
| 116 flex-direction: row; | |
| 117 background-color: rgba(0, 0, 0, 0.2); | |
| 118 padding: 2px; | |
| 87 } | 119 } |
| 88 | 120 |
| 89 #bottom { | 121 .logFetchButtonVisible { |
| 122 display: flex !important; | |
| 123 } | |
| 124 | |
| 125 .logFetchButton { | |
| 126 width: 100%; | |
| 127 height: 18px; | |
| 128 } | |
| 129 | |
| 130 .logSplitUpButton { | |
| 131 background: yellow; | |
| 132 } | |
| 133 .logSplitDownButton { | |
| 134 background: green; | |
| 135 } | |
| 136 | |
| 137 .logBottomButton { | |
| 90 background-color: lightcoral; | 138 background-color: lightcoral; |
| 91 height: 2px; | 139 } |
| 92 margin-bottom: 10px; | 140 |
| 141 #logEnd { | |
| 142 margin-bottom: 30px; | |
| 143 background-color: gray; | |
| 144 } | |
| 145 | |
| 146 .clickable-log-anchor { | |
| 147 height: 24px; | |
| 93 } | 148 } |
| 94 | 149 |
| 95 #status-bar { | 150 #status-bar { |
| 96 /* Overlay at the bottom of the page. */ | 151 /* Overlay at the bottom of the page. */ |
| 97 position: fixed; | 152 position: fixed; |
| 98 z-index: 9999; | 153 z-index: 9999; |
| 99 overflow: hidden; | 154 overflow: hidden; |
| 100 bottom: 0; | 155 bottom: 0; |
| 101 left: 0; | 156 left: 0; |
| 102 width: 100%; | 157 width: 100%; |
| 103 | 158 |
| 104 text-align: center; | 159 text-align: center; |
| 105 font-size: 16px; | 160 font-size: 16px; |
| 106 background-color: rgba(245, 245, 220, 0.7); | 161 background-color: rgba(245, 245, 220, 0.7); |
| 107 } | 162 } |
| 108 </style> | 163 </style> |
| 109 | 164 |
| 110 <google-signin-aware | |
| 111 id="aware" | |
| 112 on-google-signin-aware-success="_onSignin"></google-signin-aware> | |
| 113 | |
| 114 <rpc-client | 165 <rpc-client |
| 115 id="client" | 166 id="client" |
| 116 auto-token | 167 auto-token |
| 117 host="[[host]]"></rpc-client> | 168 host="[[host]]"></rpc-client> |
| 118 | 169 |
| 170 <!-- | |
| 171 This must be after "rpc-client" so we get the signin event after it | |
| 172 does. | |
| 173 --> | |
| 174 <google-signin-aware | |
| 175 id="aware" | |
| 176 on-google-signin-aware-success="_onSignin"></google-signin-aware> | |
| 177 | |
| 119 <!-- Stream view options. --> | 178 <!-- Stream view options. --> |
| 120 <div class="main-view"> | 179 <div id="mainView"> |
| 121 <div class="buttons"> | 180 <div id="buttons"> |
| 122 <paper-checkbox checked="{{showMetadata}}"> | 181 <!-- If we have exactly one stream, we will enable users to split. --> |
| 123 Show Metadata | 182 <template is="dom-if" if="{{showStreamingControls}}"> |
| 124 </paper-checkbox> | 183 <template is="dom-if" if="{{canSplit}}"> |
| 125 <paper-checkbox checked="{{wrapLines}}"> | 184 <paper-icon-button title="Split, load logs from end" |
| 126 Wrap Lines | 185 icon="editor:vertical-align-bottom" |
| 127 </paper-checkbox> | 186 on-tap="_splitClicked"> |
| 128 <paper-checkbox checked="{{follow}}"> | 187 </paper-icon-button> |
| 129 Follow | 188 </template> |
| 130 </paper-checkbox> | 189 |
| 190 <template is="dom-if" if="{{isSplit}}"> | |
| 191 <paper-icon-button title="Scroll to split" | |
| 192 icon="editor:vertical-align-center" on-tap="_scrollToSplit"> | |
| 193 </paper-icon-button> | |
| 194 </template> | |
| 195 | |
| 196 <paper-icon-button class="paper-icon-button-highlight" toggles | |
| 197 title="Automatically scroll to new logs." icon="icons:update" | |
| 198 active="{{follow}}"> | |
| 199 </paper-icon-button> | |
| 200 | |
| 201 <paper-icon-button class="paper-icon-button-highlight" toggles | |
| 202 title="Automatically load logs." icon="{{playingIconName}}" | |
| 203 active="{{playing}}"> | |
| 204 </paper-icon-button> | |
| 205 | |
| 206 <template is="dom-if" if="{{isSplit}}"> | |
| 207 <paper-icon-button toggles | |
| 208 title="Load new logs, or backfill from top." | |
| 209 icon="{{backfillIconName}}" | |
| 210 active="{{backfill}}"> | |
| 211 </paper-icon-button> | |
| 212 </template> | |
| 213 </template> | |
| 214 | |
| 215 <template is="dom-if" if="{{_not(playing)}}"> | |
| 216 <paper-button class="paper-button-highlight" toggles raised | |
| 217 active="{{wrapLines}}"> | |
| 218 Wrap | |
| 219 </paper-button> | |
| 220 | |
| 221 <template is="dom-if" if="{{metadata}}"> | |
| 222 <paper-button class="paper-button-highlight" toggles raised | |
| 223 active="{{showMetadata}}"> | |
| 224 Metadata | |
| 225 </paper-button> | |
| 226 </template> | |
| 227 </template> | |
| 131 </div> | 228 </div> |
| 132 | 229 |
| 133 <!-- Display current fetching status, if stream data is still loading. --> | 230 <!-- Display current fetching status, if stream data is still loading. --> |
| 134 <template is="dom-if" if="{{streamStatus}}"> | 231 <div id="streamStatus"> |
| 135 <div id="stream-status"> | 232 <template is="dom-if" if="{{streamStatus}}"> |
| 136 <table> | 233 <table> |
| 137 <template is="dom-repeat" items="{{streamStatus}}"> | 234 <template is="dom-repeat" items="{{streamStatus}}"> |
| 138 <tr> | 235 <tr> |
| 139 <td>{{item.name}}</td> | 236 <td>{{item.name}}</td> |
| 140 <td>{{item.desc}}</td> | 237 <td>{{item.desc}}</td> |
| 141 </tr> | 238 </tr> |
| 142 </template> | 239 </template> |
| 143 </table> | 240 </table> |
| 144 </div> | 241 </template> |
| 145 </template> | 242 </div> |
| 146 | 243 |
| 147 <!-- Muxed log content. --> | 244 <!-- Muxed log content. --> |
| 148 <div id="logContent" on-mousewheel="_handleMouseWheel"> | 245 <div id="logContent" |
| 246 on-mousewheel="_handleMouseWheel"> | |
| 149 <div id="logs"> | 247 <div id="logs"> |
| 150 <!-- Content will be populated with JavaScript as logs are loaded. | 248 <!-- Content will be populated with JavaScript as logs are loaded. |
| 151 | 249 |
| 152 <div class="log-entry"> | 250 <div class="log-entry"> |
| 153 <div class="log-entry-meta"> | 251 <div class="log-entry-meta"> |
| 154 <div class="log-entry-meta-line">(Meta 0)</div> | 252 <div class="log-entry-meta-line">(Meta 0)</div> |
| 155 ... | 253 ... |
| 156 <div class="log-entry-meta-line">(Meta N)</div> | 254 <div class="log-entry-meta-line">(Meta N)</div> |
| 157 </div> | 255 </div> |
| 158 <div class="log-entry-content"> | 256 <div class="log-entry-content"> |
| 159 <div class="log-entry-line">LINE #0</div> | 257 LINE #0 |
| 160 ... | 258 ... |
| 161 <div class="log-entry-line">LINE #N</div> | 259 LINE #N |
| 162 </div> | 260 </div> |
| 163 </div> | 261 </div> |
| 164 ... | 262 ... |
| 165 | 263 |
| 264 | |
| 265 Note that we can't use templating to show/hide the log dividers, | |
| 266 since our positional log insertion requires them to be present and | |
| 267 move along with insertions as points of reference. | |
| 166 --> | 268 --> |
| 269 | |
| 270 <div id="logSplit" class="logFetchButtonContainer"> | |
| 271 <!-- Insert point (prepend for head, append for tail). --> | |
| 272 <paper-button id="logSplitUp" | |
| 273 class="logFetchButton logSplitUpButton giant" | |
| 274 disabled="[[streamAnchorsNotClickable]]" | |
| 275 on-click="_handleUpClick"> | |
| 276 <iron-icon icon="file-upload"></iron-icon> | |
| 277 </paper-button> | |
| 278 <paper-button id="logSplitDown" | |
| 279 class="logFetchButton logSplitDownButton giant" | |
| 280 disabled="[[streamAnchorsNotClickable]]" | |
| 281 on-click="_handleDownClick"> | |
| 282 <iron-icon icon="file-download"></iron-icon> | |
| 283 </paper-button> | |
| 284 </div> | |
| 285 | |
| 286 <div id="logBottom" class="logFetchButtonContainer"> | |
| 287 <!-- | |
| 288 Bottom of the log stream (red bottom line). When tail is complete, | |
| 289 all future logs get prepended to this. | |
| 290 --> | |
| 291 <paper-button id="logBottomButton" | |
| 292 class="logFetchButton logBottomButton giant" | |
| 293 disabled="[[streamAnchorsNotClickable]]" | |
| 294 on-click="_handleBottomClick"> | |
| 295 <iron-icon icon="arrow-drop-down"></iron-icon> | |
| 296 </paper-button> | |
| 297 </div> | |
| 298 <div id="logEnd"></div> | |
| 167 </div> | 299 </div> |
| 168 | |
| 169 <!-- Current red bottom line. --> | |
| 170 <div id="bottom"></div> | |
| 171 </div> | 300 </div> |
| 172 | 301 |
| 173 </div> | 302 </div> |
| 174 | 303 |
| 175 <template is="dom-if" if="{{statusBar}}"> | 304 <template is="dom-if" if="{{statusBar}}"> |
| 176 <div id="status-bar">{{statusBar.value}}</div> | 305 <div id="status-bar">{{statusBar.value}}</div> |
| 177 </template> | 306 </template> |
| 178 </template> | 307 </template> |
| 179 | 308 |
| 180 </dom-module> | 309 </dom-module> |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 199 * project. For example, for stream "foo/bar/+/baz" in project "chromium", | 328 * project. For example, for stream "foo/bar/+/baz" in project "chromium", |
| 200 * the stream path would be: "chromium/foo/bar/+/baz". | 329 * the stream path would be: "chromium/foo/bar/+/baz". |
| 201 */ | 330 */ |
| 202 streams: { | 331 streams: { |
| 203 type: Array, | 332 type: Array, |
| 204 value: [], | 333 value: [], |
| 205 notify: true, | 334 notify: true, |
| 206 observer: "_streamsChanged", | 335 observer: "_streamsChanged", |
| 207 }, | 336 }, |
| 208 | 337 |
| 338 toolbarAnchor: { | |
| 339 type: Object, | |
| 340 value: null, | |
| 341 }, | |
| 342 | |
| 343 mobile: { | |
| 344 type: Boolean, | |
| 345 value: false, | |
| 346 }, | |
| 347 | |
| 209 /** | 348 /** |
| 210 * The number of logs to load before forcing a page refresh. | 349 * The number of logs to load before forcing a page refresh. |
| 211 * | 350 * |
| 212 * The smaller the value, the smoother the page will behave while logs are | 351 * The smaller the value, the smoother the page will behave while logs are |
| 213 * loading. However, the logs will also load slower because of forced | 352 * loading. However, the logs will also load slower because of forced |
| 214 * renders in between elements. | 353 * renders in between elements. |
| 215 */ | 354 */ |
| 216 burst: { | 355 burst: { |
| 217 type: Number, | 356 type: Number, |
| 218 value: 1000, | 357 value: 1000, |
| 219 notify: true, | 358 notify: true, |
| 220 }, | 359 }, |
| 221 | 360 |
| 361 /** | |
| 362 * If true, render metadata blocks alongside their log entries. | |
| 363 * | |
| 364 * This will cause significantly more HTML elements during rendering (so | |
| 365 * that each metadata element can show up next to its row) and greatly | |
| 366 * slow the viewer down. | |
| 367 */ | |
| 368 metadata: { | |
| 369 type: Boolean, | |
| 370 value: false, | |
| 371 }, | |
| 372 | |
| 222 /** If true, show log metadata column. */ | 373 /** If true, show log metadata column. */ |
| 223 showMetadata: { | 374 showMetadata: { |
| 224 type: Boolean, | 375 type: Boolean, |
| 225 value: false, | 376 value: false, |
| 226 observer: "_showMetadataChanged", | 377 observer: "_showMetadataChanged", |
| 227 }, | 378 }, |
| 228 | 379 |
| 229 /** If true, wrap log lines to the screen. */ | 380 /** If true, wrap log lines to the screen. */ |
| 230 wrapLines: { | 381 wrapLines: { |
| 231 type: Boolean, | 382 type: Boolean, |
| 232 value: false, | 383 value: false, |
| 233 observer: "_wrapLinesChanged", | 384 observer: "_wrapLinesChanged", |
| 234 }, | 385 }, |
| 235 | 386 |
| 236 /** | 387 /** |
| 237 * If true, automatically scroll the page to the bottom of the logs | 388 * If true, automatically scroll the page to the bottom of the logs |
| 238 * while they are streaming. | 389 * while they are streaming. |
| 239 */ | 390 */ |
| 240 follow: { | 391 follow: { |
| 241 type: Boolean, | 392 type: Boolean, |
| 242 value: false, | 393 value: false, |
| 243 observer: "_followChanged", | 394 observer: "_followChanged", |
| 244 }, | 395 }, |
| 245 | 396 |
| 397 canSplit: { | |
| 398 type: Boolean, | |
| 399 value: false, | |
| 400 readOnly: true, | |
| 401 }, | |
| 402 | |
| 403 showStreamingControls: { | |
| 404 type: Boolean, | |
| 405 value: true, | |
| 406 readOnly: true, | |
| 407 }, | |
| 408 | |
| 409 isSplit: { | |
| 410 type: Boolean, | |
| 411 value: false, | |
| 412 readOnly: true, | |
| 413 }, | |
| 414 | |
| 415 streamAnchorsNotClickable: { | |
| 416 type: Boolean, | |
| 417 computed: | |
| 418 '_computeAnchorsNotClickable(playing, showStreamingControls)', | |
| 419 }, | |
| 420 | |
| 421 playing: { | |
| 422 type: Boolean, | |
| 423 value: false, | |
| 424 observer: "_playingChanged", | |
| 425 }, | |
| 426 | |
| 427 playingIconName: { | |
| 428 type: String, | |
| 429 computed: '_computePlayingIconName(playing)', | |
| 430 }, | |
| 431 | |
| 432 backfill: { | |
| 433 type: Boolean, | |
| 434 value: false, | |
| 435 observer: "_backfillChanged", | |
| 436 }, | |
| 437 | |
| 438 backfillIconName: { | |
| 439 type: String, | |
| 440 computed: '_computeBackfillIconName(backfill)', | |
| 441 }, | |
| 442 | |
| 246 /** | 443 /** |
| 247 * The current stream status. This is an Array of objects: | 444 * The current stream status. This is an Array of objects: |
| 248 * obj.name is the name of the stream. | 445 * obj.name is the name of the stream. |
| 249 * obj.desc is the status description of the stream. | 446 * obj.desc is the status description of the stream. |
| 250 */ | 447 */ |
| 251 streamStatus: { | 448 streamStatus: { |
| 252 type: String, | 449 type: String, |
| 253 value: null, | 450 value: null, |
| 254 notify: true, | 451 notify: true, |
| 255 readOnly: true, | 452 readOnly: true, |
| 256 }, | 453 }, |
| 257 | 454 |
| 258 /** | 455 /** |
| 259 * The text content of the status element at the bottom of the page. | 456 * The text content of the status element at the bottom of the page. |
| 260 */ | 457 */ |
| 261 statusBar: { | 458 statusBar: { |
| 262 type: String, | 459 type: String, |
| 263 value: null, | 460 value: null, |
| 264 readOnly: true, | 461 readOnly: true, |
| 265 }, | 462 }, |
| 266 }, | 463 }, |
| 267 | 464 |
| 268 ready: function() { | 465 created: function() { |
| 269 this._scheduledWrite = null; | 466 this._scrollTimeoutId = null; |
| 270 this._buffer = null; | 467 |
| 271 this._currentLogBuffer = null; | 468 // Create "onScrollHandler", which just invokes "_onScroll" while bound |
| 272 this._authCallback = null; | 469 // to "this". We create it here so we can unregister it later, since |
| 470 // "bind" returns a modified value. | |
| 471 this._onScrollHandler = function(e) { this._onScroll(e); }.bind(this); | |
| 472 }, | |
| 473 | |
| 474 attached: function() { | |
| 475 require(["inc/rpc/client", "inc/logdog-stream-view/viewer"], | |
| 476 function(client, viewer) { | |
| 477 // Instantiate our view, and install callbacks. | |
| 478 this._v = viewer; | |
| 479 this._model = new viewer.Model({ | |
| 480 client: new client.luci_rpc.Client(this.$.client), | |
| 481 mobile: this.mobile, | |
| 482 | |
| 483 pushLogEntries: this._pushLogEntries.bind(this), | |
| 484 clearLogEntries: this._clearLogEntries.bind(this), | |
| 485 updateControls: this._updateControls.bind(this), | |
| 486 locationIsVisible: this._locationIsVisible.bind(this), | |
| 487 }); | |
| 488 this._streamsChanged(); | |
| 489 }.bind(this)); | |
| 490 | |
| 491 window.addEventListener('scroll', this._onScrollHandler); | |
| 273 }, | 492 }, |
| 274 | 493 |
| 275 detached: function() { | 494 detached: function() { |
| 276 this.stop(); | 495 // Unregsiter event handlers. |
| 496 window.removeEventListener('scroll', this._onScrollHandler); | |
| 497 | |
| 498 // Reset state. | |
| 499 this.reset(); | |
| 277 }, | 500 }, |
| 278 | 501 |
| 279 stop: function() { | 502 stop: function() { |
| 280 this._cancelFetch(true); | 503 this.reset(); |
| 281 }, | 504 }, |
| 282 | 505 |
| 283 /** Clears state and begins fetching log data. */ | 506 /** Clears state and begins fetching log data. */ |
| 284 reset: function() { | 507 reset: function() { |
| 285 this._resetLogState(); | 508 if ( this._model ) { |
| 286 | 509 this._model.reset(); |
| 287 this._resolveStreams().then(function(streams) { | 510 } |
| 288 this._resetToStreams(streams); | 511 this._resetScroll(); |
| 289 }.bind(this)).catch(function(error) { | 512 this._model = null; |
| 290 this._loadStatusBar("Failed to resolve streams:" + error); | 513 this._renderedLogs = false; |
| 291 throw error; | 514 }, |
| 515 | |
| 516 /** | |
| 517 * Called each time a scroll event is fired. Since this can be really | |
| 518 * frequent, this will kick off a "scroll handler" in the background at an | |
| 519 * interval. Multiple scroll events within that interval will only result | |
| 520 * in one scroll handler invocation. | |
| 521 */ | |
| 522 _onScroll: function(e) { | |
| 523 if ( this._scrollTimeoutId ) { | |
| 524 return; | |
| 525 } | |
| 526 | |
| 527 window.setTimeout(function() { | |
| 528 this._handleScrollEvent(e); | |
| 529 }.bind(this), 100); | |
| 530 }, | |
| 531 | |
| 532 /** Actual scroll event handler. */ | |
| 533 _handleScrollEvent: function(e) { | |
| 534 this._resetScroll(); | |
| 535 | |
| 536 // Update our button bar position to be relative to the parent's height. | |
| 537 this._adjustToTop(this.$.buttons); | |
| 538 this._adjustToTop(this.$.streamStatus); | |
| 539 }, | |
| 540 | |
| 541 _adjustToTop: function(elem) { | |
| 542 // Update our button bar position to be relative to the parent's height. | |
| 543 var pageRect = this.$.mainView.getBoundingClientRect(); | |
| 544 var elemRect = elem.getBoundingClientRect(); | |
| 545 var adjusted = (elem.offsetTop + pageRect.top - elemRect.top); | |
| 546 if ( adjusted < 0 ) { | |
| 547 adjusted = 0; | |
| 548 } | |
| 549 elem.style.top = adjusted; | |
| 550 }, | |
| 551 | |
| 552 /** Clears asynchornous scroll event status. */ | |
| 553 _resetScroll: function() { | |
| 554 if ( this._scrollTimeoutId !== null ) { | |
| 555 window.clearTimeout(this._scrollTimeoutId); | |
| 556 this._scrollTimeoutId = null; | |
| 557 } | |
| 558 }, | |
| 559 | |
| 560 _handleMouseWheel: function(e) { | |
| 561 this.follow = false; | |
| 562 }, | |
| 563 | |
| 564 _handleDownClick: function(e) { | |
| 565 this._model.fetchLocation(this._v.Location.HEAD, true); | |
| 566 }, | |
| 567 | |
| 568 _handleUpClick: function(e) { | |
| 569 this._model.fetchLocation(this._v.Location.TAIL, true); | |
| 570 }, | |
| 571 | |
| 572 _handleBottomClick: function(e) { | |
| 573 this._model.fetchLocation(this._v.Location.BOTTOM, true); | |
| 574 }, | |
| 575 | |
| 576 /** Called when the bound log stream variables has changed. */ | |
| 577 _streamsChanged: function() { | |
| 578 if( ! this._model ) { | |
| 579 return; | |
| 580 } | |
| 581 | |
| 582 this._model.resolve(this.streams).then( function() { | |
| 583 // If we're not on mobile, start with playing state. | |
| 584 this.playing = (!this.mobile); | |
| 585 | |
| 586 // Perform the initial fetch after resolution. | |
| 587 this._model.setAutomatic( this.playing ); | |
| 588 this._model.setTailing( ! this.backfill ); | |
| 589 this._model.fetch(false); | |
| 590 }.bind(this) ); | |
| 591 }, | |
| 592 | |
| 593 _appendMetaLine: function(root, key, value) { | |
| 594 var line = document.createElement("div"); | |
| 595 line.className = "log-entry-meta-line"; | |
| 596 | |
| 597 if ( key != null ) { | |
| 598 var e = document.createElement("strong"); | |
| 599 e.textContent = key; | |
| 600 line.appendChild(e); | |
| 601 } | |
| 602 | |
| 603 if ( value != null ) { | |
| 604 var e = document.createElement("span"); | |
| 605 e.textContent = value; | |
| 606 line.appendChild(e); | |
| 607 } | |
| 608 | |
| 609 root.appendChild(line); | |
| 610 }, | |
| 611 | |
| 612 _pushLogEntries: function(entries, insertion) { | |
| 613 // Mark that we've rendered logs (show bars now). | |
| 614 this._renderedLogs = true; | |
| 615 | |
| 616 // Build our log entry chunk. | |
| 617 var logEntryChunk = document.createElement("div"); | |
| 618 logEntryChunk.className = "log-entry-chunk"; | |
| 619 | |
| 620 var lastLogEntry = logEntryChunk; | |
| 621 var lines = new Array(); | |
| 622 | |
| 623 entries.forEach(function(le) { | |
| 624 var text = le.text; | |
| 625 if (!(text && text.lines)) { | |
| 626 return; | |
| 627 } | |
| 628 | |
| 629 // If we're rendering metadata, render an element per log entry. | |
| 630 if( this.metadata ) { | |
| 631 var entryRow = document.createElement("div"); | |
| 632 entryRow.className = "log-entry"; | |
| 633 | |
| 634 // Metadata column. | |
| 635 var metadataBlock = document.createElement("div"); | |
| 636 metadataBlock.className = "log-entry-meta"; | |
| 637 | |
| 638 this._appendMetaLine(metadataBlock, "Timestamp:", le.timestamp); | |
| 639 this._appendMetaLine(metadataBlock, "Stream:", le.desc.name); | |
| 640 this._appendMetaLine(metadataBlock, "Index:", le.streamIndex); | |
| 641 | |
| 642 // Log column. | |
| 643 var logDataBlock = document.createElement("div"); | |
| 644 logDataBlock.className = "log-entry-content"; | |
| 645 | |
| 646 le.text.lines.forEach(function(line) { | |
| 647 lines.push(line.value); | |
| 648 }); | |
| 649 | |
| 650 logDataBlock.textContent = lines.join("\n"); | |
| 651 lines.length = 0; | |
| 652 | |
| 653 entryRow.appendChild(metadataBlock); | |
| 654 entryRow.appendChild(logDataBlock); | |
| 655 | |
| 656 logEntryChunk.appendChild(entryRow); | |
| 657 lastLogEntry = entryRow; | |
| 658 } else { | |
| 659 // Add this to the lines. We'll assign this directly to logEntryChunk | |
| 660 // after the loop. | |
| 661 le.text.lines.forEach(function(line) { | |
| 662 lines.push(line.value); | |
| 663 }); | |
| 664 } | |
| 292 }.bind(this)); | 665 }.bind(this)); |
| 293 }, | 666 |
| 294 | 667 if ( ! this.metadata ) { |
| 295 /** Clears all current logs. */ | 668 // Only one HTML element: the chunk. |
| 296 _resetLogState: function() { | 669 logEntryChunk.textContent = lines.join("\n"); |
| 297 this._cancelFetch(true); | 670 lastLogEntry = logEntryChunk; |
| 298 | 671 } |
| 672 | |
| 673 // To have styles apply correctly, we need to add it twice, see | |
| 674 // https://github.com/Polymer/polymer/issues/3100. | |
| 675 Polymer.dom(this.root).appendChild(logEntryChunk); | |
| 676 | |
| 677 // Add the log entry to the appropriate place. | |
| 678 var anchor, scrollToTop = false; | |
| 679 var forceScroll = false; | |
| 680 switch ( insertion ) { | |
| 681 case this._v.Location.HEAD: | |
| 682 // InsertionPoint.HEAD: PREPEND to "logSplit". | |
| 683 this.$.logs.insertBefore(logEntryChunk, this.$.logSplit); | |
| 684 | |
| 685 // If we're not split, scroll to the log bottom. Otherwise, scroll to | |
| 686 // the split. | |
| 687 anchor = lastLogEntry; | |
| 688 break; | |
| 689 | |
| 690 case this._v.Location.TAIL: | |
| 691 // InsertionPoint.TAIL: APPEND to "logSplit". | |
| 692 anchor = this.$.logSplit; | |
| 693 | |
| 694 // Identify the element *after* our insertion point and scroll to it. | |
| 695 // This provides a semblance of stability as we top-insert. | |
| 696 // | |
| 697 // As a special case, if the next element is the log bottom, just | |
| 698 // scroll to the split, since there is no content to stabilize. | |
| 699 if ( anchor.nextElementSibling !== this.$.logBottom ) { | |
| 700 anchor = anchor.nextElementSibling; | |
| 701 } | |
| 702 | |
| 703 // Insert logs by adding them before the sibling following the log | |
| 704 // split (append to this.$.logSplit). | |
| 705 this.$.logs.insertBefore(logEntryChunk, this.$.logSplit.nextSibling); | |
| 706 | |
| 707 // When tailing, always scroll to the anchor point. | |
| 708 scrollToTop = true; | |
| 709 forceScroll = true; | |
| 710 break; | |
| 711 | |
| 712 case this._v.Location.BOTTOM: | |
| 713 // InsertionPoint.BOTTOM: PREPEND to "logBottom". | |
| 714 anchor = this.$.logBottom; | |
| 715 this.$.logs.insertBefore(logEntryChunk, anchor); | |
| 716 break; | |
| 717 } | |
| 718 | |
| 719 this._maybeScrollToElement(anchor, scrollToTop, forceScroll); | |
| 720 }, | |
| 721 | |
| 722 _clearLogEntries: function() { | |
| 299 // Remove all current log elements. */ | 723 // Remove all current log elements. */ |
| 300 while (this.$.logs.hasChildNodes()) { | 724 for ( var cur = this.$.logs.firstChild; cur; ) { |
| 301 this.$.logs.removeChild(this.$.logs.lastChild); | 725 var del = cur; |
| 302 } | 726 cur = cur.nextElementSibling; |
| 303 | 727 if ( del.classList && del.classList.contains('log-entry-chunk') ) { |
| 304 // Clear our buffer and streamer state. | 728 this.$.logs.removeChild(del); |
| 305 this._buffer = null; | 729 } |
| 306 this._currentLogBuffer = null; | 730 } |
| 307 if (this._streamer) { | 731 }, |
| 308 this._streamer.shutdown(); | 732 |
| 309 } | 733 _locationIsVisible: function(l) { |
| 310 this._streamer = null; | 734 var anchor; |
| 311 }, | 735 switch( l ) { |
| 312 | 736 case this._v.Location.HEAD: |
| 313 _resolveStreams: function() { | 737 case this._v.Location.TAIL: |
| 314 // Separate our configured streams into full stream paths and queries. | 738 anchor = this.$.logSplit; |
| 315 var parts = { | 739 |
| 316 queries: [], | 740 case this._v.Location.BOTTOM: |
| 317 streams: [], | 741 anchor = this.$.logBottom; |
| 318 }; | 742 |
| 319 var query = new LogDogQuery(this.project); | 743 default: |
| 320 this.streams.map(LogDogStream.splitProject).forEach(function(v) { | 744 return false; |
| 321 if (LogDogQuery.isQuery(v.path)) { | 745 } |
| 322 parts.queries.push(v); | 746 return this._elementInViewport(anchor); |
| 747 }, | |
| 748 | |
| 749 _updateControls: function(c) { | |
| 750 this._setCanSplit(c.canSplit); | |
| 751 this._setIsSplit(c.split); | |
| 752 this.toggleClass("logFetchButtonVisible", | |
| 753 (c.split && this._renderedLogs), this.$.logSplit); | |
| 754 this.toggleClass("logFetchButtonVisible", | |
| 755 (c.bottom && this._renderedLogs), this.$.logBottom); | |
| 756 | |
| 757 this._setShowStreamingControls( !c.fullyLoaded ); | |
| 758 if ( c.fullyLoaded ) { | |
| 759 this.playing = false; | |
| 760 } | |
| 761 | |
| 762 switch( c.loadingState ) { | |
| 763 case this._v.LoadingState.NONE: | |
| 764 this._loadStatusBar(null); | |
| 765 break; | |
| 766 case this._v.LoadingState.RESOLVING: | |
| 767 this._loadStatusBar("Resolving stream names..."); | |
| 768 break; | |
| 769 case this._v.LoadingState.LOADING: | |
| 770 this._loadStatusBar("Loading streams..."); | |
| 771 break; | |
| 772 case this._v.LoadingState.RENDERING: | |
| 773 this._loadStatusBar("Rendering logs."); | |
| 774 break; | |
| 775 case this._v.LoadingState.NEEDS_AUTH: | |
| 776 this._loadStatusBar("Not authenticated. Please log in."); | |
| 777 break; | |
| 778 case this._v.LoadingState.ERROR: | |
| 779 this._loadStatusBar("Error loading streams (see console)."); | |
| 780 break; | |
| 781 } | |
| 782 | |
| 783 this._setStreamStatus(c.streamStatus); | |
| 784 }, | |
| 785 | |
| 786 /** Scrolls to the follow anchor point. */ | |
| 787 _maybeScrollToFollow: function() { | |
| 788 // Determine our anchor element. | |
| 789 var e; | |
| 790 if ( this.isSplit && this.backfill ) { | |
| 791 // Centering on the split element, at the bottom of the page. | |
| 792 e = this.$.logSplit; | |
| 793 } else { | |
| 794 // Scroll to the bottom of the page. | |
| 795 e = this.$.logEnd; | |
| 796 } | |
| 797 | |
| 798 this._maybeScrollToElement(e, false, false); | |
| 799 }, | |
| 800 | |
| 801 /** | |
| 802 * Scrolls to the specified element, centering it at the top or bottom of | |
| 803 * the view. By default,t his will only happen if "follow" is enabled; | |
| 804 * however, it can be forced via "force". | |
| 805 */ | |
| 806 _maybeScrollToElement: function(element, topOfView, force) { | |
| 807 if (this.follow || force) { | |
| 808 if ( topOfView ) { | |
| 809 element.scrollIntoView({ | |
| 810 behavior: "auto", | |
| 811 block: "end", | |
| 812 }); | |
| 323 } else { | 813 } else { |
| 324 parts.streams.push(v); | 814 // Bug? "block: start" doesn't seem to work the same as false. |
| 325 } | 815 element.scrollIntoView(false); |
| 326 }); | 816 } |
| 327 | 817 } |
| 328 // Resolve any outstanding queries into full stream paths. | 818 }, |
| 329 // | 819 |
| 330 // If we get an authentication error, register to have our query | |
| 331 // resolution callback invoked on signin changes until it works (or | |
| 332 // indefinitely). | |
| 333 var queries = parts.queries.map(function(v) { | |
| 334 var params = new LogDogQueryParams(v.project). | |
| 335 path(v.path). | |
| 336 streamType("text"); | |
| 337 return new LogDogQuery(this.$.client, params); | |
| 338 }.bind(this)); | |
| 339 | |
| 340 var issueQuery = function() { | |
| 341 this._loadStatusBar("Resolving log streams from query..."); | |
| 342 this._authCallback = null; | |
| 343 | |
| 344 return Promise.all(queries.map(function(q) { | |
| 345 return q.getAll(); | |
| 346 }.bind(this))).then(function(results) { | |
| 347 this._loadStatusBar(null); | |
| 348 | |
| 349 // Add query results (if any) to streams. | |
| 350 results.forEach(function(streams) { | |
| 351 (streams || []).forEach(function(stream) { | |
| 352 parts.streams.push(stream.stream); | |
| 353 }); | |
| 354 }); | |
| 355 parts.streams.sort(LogDogStream.cmp); | |
| 356 | |
| 357 // Remove any duplicates. | |
| 358 var seenStreams = {}; | |
| 359 var result = []; | |
| 360 parts.streams.forEach(function(s) { | |
| 361 var fullName = s.fullName(); | |
| 362 if (!seenStreams[fullName]) { | |
| 363 seenStreams[fullName] = s; | |
| 364 result.push(s); | |
| 365 } | |
| 366 }); | |
| 367 return result; | |
| 368 }.bind(this)).catch(function(error) { | |
| 369 if (error instanceof LogDogError && error.isUnauthenticated()) { | |
| 370 // Retry on auth event. | |
| 371 this._loadStatusBar("Not authorized to execute query. Log in " + | |
| 372 "with an authorized account."); | |
| 373 return new Promise(function(resolve) { | |
| 374 this._authCallback = resolve; | |
| 375 }.bind(this)).then(issueQuery); | |
| 376 } | |
| 377 | |
| 378 throw error; | |
| 379 }.bind(this)); | |
| 380 }.bind(this); | |
| 381 return issueQuery(); | |
| 382 }, | |
| 383 | |
| 384 _resetToStreams: function(streams) { | |
| 385 // Unique streams. | |
| 386 if (!streams.length) { | |
| 387 this._loadStatusBar("No log streams."); | |
| 388 return; | |
| 389 } | |
| 390 | |
| 391 console.log("Loading log streams:", streams); | |
| 392 this._loadStatusBar("Loading stream data..."); | |
| 393 streams.sort(LogDogStream.cmp); | |
| 394 | |
| 395 // Create a _BufferedStream for each stream. | |
| 396 var bufStreams = streams.map(function(stream, idx) { | |
| 397 return new _BufferedStream(stream, this.$.client, | |
| 398 (streams.length > 1), function(bs) { | |
| 399 this._updateStreamStatus(bs, idx); | |
| 400 }.bind(this)); | |
| 401 }.bind(this)); | |
| 402 this._buffer = new _LogStreamBuffer(); | |
| 403 this._buffer.setStreams(bufStreams) | |
| 404 | |
| 405 this._streamer = new _LogStreamer(this._buffer, this.burst, function(v) { | |
| 406 this._loadStatusBar(v); | |
| 407 }.bind(this)); | |
| 408 | |
| 409 // Construct our initial status content. | |
| 410 this._setStreamStatus(bufStreams.map(function(bs, idx) { | |
| 411 return { | |
| 412 name: (" [.../+/" + bs.stream.name() + "]"), | |
| 413 desc: bs.description(), | |
| 414 }; | |
| 415 }.bind(this))); | |
| 416 | |
| 417 // Kick off our log fetching. | |
| 418 this._scheduleWriteNextLogs(); | |
| 419 }, | |
| 420 | |
| 421 /** Cancels any currently-executing log stream fetch. */ | |
| 422 _cancelFetch: function(clear) { | |
| 423 this._cancelScheduledWrite(); | |
| 424 this._authCallback = null; | |
| 425 | |
| 426 if (clear) { | |
| 427 this._setStreamStatus(null); | |
| 428 this._loadStatusBar(null); | |
| 429 } | |
| 430 }, | |
| 431 | |
| 432 /** Cancels any scheduled asynchronous write. */ | |
| 433 _cancelScheduledWrite: function() { | |
| 434 if (this._scheduledWrite) { | |
| 435 this.cancelAsync(this._scheduledWrite); | |
| 436 this._scheduledWrite = null; | |
| 437 } | |
| 438 }, | |
| 439 | |
| 440 /** Called when the bound log stream variables has changed. */ | |
| 441 _streamsChanged: function(v, old) { | |
| 442 this.reset(); | |
| 443 }, | |
| 444 | |
| 445 /** Schedules the next asynchronous log write. */ | |
| 446 _scheduleWriteNextLogs: function() { | |
| 447 // This is called after refresh, so use this opportunity to maybe scroll | |
| 448 // to the bottom. | |
| 449 this._maybeScrollToBottom(); | |
| 450 | |
| 451 if (! this._scheduledWrite) { | |
| 452 this._scheduledWrite = this.async(function() { | |
| 453 this._writeNextLogs() | |
| 454 }.bind(this)); | |
| 455 } | |
| 456 }, | |
| 457 | |
| 458 /** | |
| 459 * This is an iterative function that grabs the next set of logs and renders | |
| 460 * them. Afterwards, it will continue rescheduling itself until there are | |
| 461 * no more logs to render. | |
| 462 */ | |
| 463 _writeNextLogs: function() { | |
| 464 this._cancelScheduledWrite(); | |
| 465 | |
| 466 this._streamer.load().then(function(entries) { | |
| 467 // If there are no entries, then we're done. | |
| 468 if (! entries) { | |
| 469 // Cancel all fetching state. If our streamer is finished, also clear | |
| 470 // messages and status. | |
| 471 if (this._streamer.finished) { | |
| 472 if (this._streamer.someStreamsFailed) { | |
| 473 this._cancelFetch(false); | |
| 474 this._loadStatusBar("Some streams failed to load."); | |
| 475 } else { | |
| 476 this._cancelFetch(true); | |
| 477 } | |
| 478 } else { | |
| 479 // No more logs, but also we are not finished. Retry after auth. | |
| 480 this._authCallback = this._scheduleWriteNextLogs.bind(this); | |
| 481 } | |
| 482 return; | |
| 483 } | |
| 484 | |
| 485 var logEntryChunk = document.createElement("div"); | |
| 486 entries.forEach(function(le) { | |
| 487 this._appendLogEntry(logEntryChunk, le); | |
| 488 }.bind(this)); | |
| 489 | |
| 490 // To have styles apply correctly, we need to add it twice, see | |
| 491 // https://github.com/Polymer/polymer/issues/3100. | |
| 492 Polymer.dom(this.root).appendChild(logEntryChunk); | |
| 493 this.$.logs.appendChild(logEntryChunk); | |
| 494 | |
| 495 // Yield so that our browser can refresh. We can't directly use | |
| 496 // this.async since a timeout of "0" causes immediate execution instead | |
| 497 // of yielding. | |
| 498 setTimeout(function() { | |
| 499 this._scheduleWriteNextLogs(); | |
| 500 }.bind(this), 0); | |
| 501 }.bind(this)); | |
| 502 }, | |
| 503 | |
| 504 _appendLogEntry: function(root, le) { | |
| 505 var text = le.text; | |
| 506 if (!(text && text.lines)) { | |
| 507 return 0; | |
| 508 } | |
| 509 | |
| 510 // Create elements manually to avoid Polymer overhead for large logs. | |
| 511 var entryRow = document.createElement("div"); | |
| 512 entryRow.className = "log-entry"; | |
| 513 | |
| 514 // Metadata column. | |
| 515 var metadataBlock = document.createElement("div"); | |
| 516 metadataBlock.className = "log-entry-meta"; | |
| 517 entryRow.appendChild(metadataBlock); | |
| 518 | |
| 519 var timestampDiv = document.createElement("div"); | |
| 520 timestampDiv.className = "log-entry-meta-line"; | |
| 521 timestampDiv.textContent = le.timestamp; | |
| 522 metadataBlock.appendChild(timestampDiv); | |
| 523 | |
| 524 var nameDiv = document.createElement("div"); | |
| 525 nameDiv.className = "log-entry-meta-line"; | |
| 526 nameDiv.textContent = le.desc.name; | |
| 527 metadataBlock.appendChild(nameDiv); | |
| 528 | |
| 529 var streamDiv = document.createElement("div"); | |
| 530 streamDiv.className = "log-entry-meta-line"; | |
| 531 streamDiv.textContent = le.streamIndex; | |
| 532 metadataBlock.appendChild(streamDiv); | |
| 533 | |
| 534 // Log column. | |
| 535 var logDataBlock = document.createElement("div"); | |
| 536 logDataBlock.className = "log-entry-content"; | |
| 537 if (le.text) { | |
| 538 for (var i = 0; i < le.text.lines.length; i++) { | |
| 539 var lineDiv = document.createElement("div"); | |
| 540 lineDiv.className = "log-entry-line"; | |
| 541 lineDiv.textContent = le.text.lines[i].value; | |
| 542 logDataBlock.appendChild(lineDiv); | |
| 543 } | |
| 544 } | |
| 545 entryRow.appendChild(logDataBlock); | |
| 546 root.appendChild(entryRow); | |
| 547 | |
| 548 return le.text.lines.length; | |
| 549 }, | |
| 550 | |
| 551 _updateStreamStatus: function(bs, idx) { | |
| 552 var origStatus = this.streamStatus[idx]; | |
| 553 this.splice("streamStatus", idx, 1, { | |
| 554 name: origStatus.name, | |
| 555 desc: bs.description(), | |
| 556 }); | |
| 557 }, | |
| 558 | |
| 559 /** Scrolls to the bottom if "follow" is enabled. */ | |
| 560 _maybeScrollToBottom: function() { | |
| 561 if (this.follow) { | |
| 562 this.$.bottom.scrollIntoView({ | |
| 563 "behavior": "smooth", | |
| 564 "block": "end", | |
| 565 }); | |
| 566 } | |
| 567 }, | |
| 568 | |
| 569 /** | 820 /** |
| 570 * Callback when "showMetadata" has changed. This adds/removes the | 821 * Callback when "showMetadata" has changed. This adds/removes the |
| 571 * "showMeta" CSS class from the metadata column. | 822 * "showMeta" CSS class from the metadata column. |
| 572 */ | 823 */ |
| 573 _showMetadataChanged: function(v) { | 824 _showMetadataChanged: function(v) { |
| 574 this.toggleClass("showMeta", v, this.$.logs); | 825 this.toggleClass("showMeta", v, this.$.logs); |
| 575 }, | 826 }, |
| 827 | |
| 576 /** | 828 /** |
| 577 * Callback when "wrapLines" has changed. This adds/removes the | 829 * Callback when "wrapLines" has changed. This adds/removes the |
| 578 * "wrapLines" CSS class to the log data. | 830 * "wrapLines" CSS class to the log data. |
| 579 */ | 831 */ |
| 580 _wrapLinesChanged: function(v) { | 832 _wrapLinesChanged: function(v) { |
| 581 this.toggleClass("wrapLines", v, this.$.logs); | 833 this.toggleClass("wrapLines", v, this.$.logs); |
| 582 }, | 834 }, |
| 835 | |
| 836 /** Callback when "follow" has changed. */ | |
| 837 _playingChanged: function(v) { | |
| 838 if( ! this._model ) { | |
| 839 return; | |
| 840 } | |
| 841 | |
| 842 // If we're playing, begin log fetching. | |
| 843 this._model.setAutomatic(v); | |
| 844 }, | |
| 845 | |
| 846 _computePlayingIconName: function(playing) { | |
| 847 return ( (playing) ? | |
|
Ryan Tseng
2016/12/06 03:38:11
is this backwards?
| |
| 848 "av:pause-circle-outline" : "av:play-circle-outline" ); | |
| 849 }, | |
| 850 | |
| 851 /** Callback when "follow" has changed. */ | |
| 852 _backfillChanged: function(v) { | |
| 853 if( ! this._model ) { | |
| 854 return; | |
| 855 } | |
| 856 | |
| 857 // If we're backfilling, then we're not tailing. | |
| 858 this._model.setTailing(!v); | |
| 859 }, | |
| 860 | |
| 861 _computeBackfillIconName: function(backfill) { | |
| 862 return ( (backfill) ? | |
| 863 "editor:border-bottom" : "editor:border-top" ); | |
| 864 }, | |
| 865 | |
| 583 /** Callback when "follow" has changed. */ | 866 /** Callback when "follow" has changed. */ |
| 584 _followChanged: function(v) { | 867 _followChanged: function(v) { |
| 585 this._maybeScrollToBottom(); | 868 if ( ! this._model ) { |
| 586 }, | 869 return; |
| 870 } | |
| 587 | 871 |
| 588 /** Callback for when the mouse wheel has scrolled. Disables follow. */ | 872 if ( v ) { |
| 589 _handleMouseWheel: function() { | 873 // If follow is toggled on, automatically begin playing. |
| 590 this.follow = false; | 874 this.playing = true; |
| 875 this._maybeScrollToFollow(); | |
| 876 } | |
| 877 }, | |
| 878 | |
| 879 /** Callback when "split" button has been clicked. */ | |
| 880 _splitClicked: function() { | |
| 881 if( ! this._model ) { | |
| 882 return; | |
| 883 } | |
| 884 | |
| 885 // After a split, toggle off playing. | |
| 886 this._model.split(); | |
| 887 this._model.setTailing(true); | |
| 888 this.playing = false; | |
| 889 }, | |
| 890 | |
| 891 /** Callback when "split" button has been clicked. */ | |
| 892 _scrollToSplit: function() { | |
| 893 this._maybeScrollToElement(this.$.logSplit, true, true); | |
| 894 }, | |
| 895 | |
| 896 _computeAnchorsNotClickable: function(playing, showStreamingControls, | |
| 897 rendering) { | |
| 898 // Anchors are not clickable if we're playing or the controls are | |
| 899 // not visible. | |
| 900 return ( playing || (!showStreamingControls) || rendering ); | |
| 901 }, | |
| 902 | |
| 903 /** Filter function to invert a value. */ | |
| 904 _not: function(v) { | |
| 905 return (!v); | |
| 591 }, | 906 }, |
| 592 | 907 |
| 593 /** | 908 /** |
| 594 * Loads text content into the status bar. | 909 * Loads text content into the status bar. |
| 595 * | 910 * |
| 596 * If null is passed, the status bar will be cleared. If text is passed, the | 911 * If null is passed, the status bar will be cleared. If text is passed, the |
| 597 * status bar will become visible with the supplied content. | 912 * status bar will become visible with the supplied content. |
| 598 */ | 913 */ |
| 599 _loadStatusBar: function(v) { | 914 _loadStatusBar: function(v) { |
| 600 var st = null; | 915 var st = null; |
| 601 if (v) { | 916 if (v) { |
| 602 st = { | 917 st = { |
| 603 value: v, | 918 value: v, |
| 604 }; | 919 }; |
| 605 } | 920 } |
| 606 this._setStatusBar(st); | 921 this._setStatusBar(st); |
| 607 }, | 922 }, |
| 608 | 923 |
| 609 _onSignin: function() { | 924 _onSignin: function() { |
| 610 var fn = this._authCallback; | 925 if ( this._model ) { |
| 611 if (fn) { | 926 this._model.notifyAuthenticationChanged(); |
| 612 this._authCallback = null; | |
| 613 fn(); | |
| 614 } | 927 } |
| 615 }, | 928 }, |
| 616 }); | |
| 617 | 929 |
| 618 /** | 930 _elementInViewport: function(el) { |
| 619 * Continuously loads log streams from a _LogStreamBuffer and exposes them via | 931 var top = el.offsetTop; |
| 620 * callback. | 932 var left = el.offsetLeft; |
| 621 */ | 933 var width = el.offsetWidth; |
| 622 function _LogStreamer(buffer, burst, statusCallback) { | 934 var height = el.offsetHeight; |
| 623 this._buffer = buffer; | |
| 624 this._burst = (burst || 0); | |
| 625 this._missingDelay = 5000; | |
| 626 this._statusCallback = statusCallback; | |
| 627 | 935 |
| 628 this.finished = false; | 936 while(el.offsetParent) { |
| 629 this.someStreamsFailed = false; | 937 el = el.offsetParent; |
| 630 | 938 top += el.offsetTop; |
| 631 this._currentLogBuffer = null; | 939 left += el.offsetLeft; |
| 632 } | |
| 633 | |
| 634 _LogStreamer.prototype.shutdown = function() { | |
| 635 this.finshed = true; | |
| 636 }; | |
| 637 | |
| 638 _LogStreamer.prototype._setStatus = function(v) { | |
| 639 if (this._statusCallback) { | |
| 640 this._statusCallback(v); | |
| 641 } | |
| 642 }; | |
| 643 | |
| 644 _LogStreamer.prototype.load = function() { | |
| 645 if (this.finished) { | |
| 646 this._setStatus(null); | |
| 647 return Promise.resolve(null); | |
| 648 } | |
| 649 | |
| 650 // If we have buffered logs, return them. | |
| 651 var current = this._currentLogBuffer; | |
| 652 if (current) { | |
| 653 // We will track how many log entries that we've rendered. If we exceed | |
| 654 // this amount, we will force a refresh so the logs appear streaming and | |
| 655 // the app remains responsive. | |
| 656 var rendered = 0; | |
| 657 | |
| 658 var entries = []; | |
| 659 for (var le = current.next(); (le); le = current.next()) { | |
| 660 entries.push(le); | |
| 661 if (le.text && le.text.lines) { | |
| 662 rendered += le.text.lines.length; | |
| 663 } | |
| 664 | |
| 665 if (this._burst > 0 && rendered >= this._burst) { | |
| 666 break; | |
| 667 } | |
| 668 } | 940 } |
| 669 | 941 |
| 670 // Have we exhausted this buffer? | 942 return ( |
| 671 if (! current.peek()) { | 943 top < (window.pageYOffset + window.innerHeight) && |
| 672 this._currentLogBuffer = null; | 944 left < (window.pageXOffset + window.innerWidth) && |
| 673 } | 945 (top + height) > window.pageYOffset && |
| 946 (left + width) > window.pageXOffset | |
| 947 ); | |
| 948 }, | |
| 674 | 949 |
| 675 // Return the bundle of entries. | 950 }); |
| 676 return Promise.resolve(entries); | |
| 677 } | |
| 678 | |
| 679 // We didn't have any buffered logs, so either all of our streams are | |
| 680 // finished or our buffer is empty and needs to be refreshed. | |
| 681 this._setStatus("Loading log stream data..."); | |
| 682 return this._buffer.nextBuffer().then(function(buf) { | |
| 683 this.someStreamsFailed = (!!this._buffer._failures.length); | |
| 684 | |
| 685 // Check result. | |
| 686 if (buf === null) { | |
| 687 if (this._buffer.finished) { | |
| 688 // No more buffers, we are done. | |
| 689 console.log("All streams have been exhausted."); | |
| 690 this.finished = true; | |
| 691 this._setStatus(null); | |
| 692 return null; | |
| 693 } | |
| 694 | |
| 695 // The buffer was incomplete. Should we retry after a delay, or do | |
| 696 // we need to wait for an explicit edge (e.g., auth)? | |
| 697 if (this._buffer.autoRetry) { | |
| 698 // Sleep for 5 seconds and try again (waiting). | |
| 699 console.log("Log stream delayed; sleeping", this._missingDelay, | |
| 700 "and retry."); | |
| 701 this._setStatus("Missing log streams, retrying after delay..."); | |
| 702 return new LuciSleepPromise(this._missingDelay).then(function() { | |
| 703 if (this.finished) { | |
| 704 console.log("Streamer is deactivated, discarding."); | |
| 705 return null; | |
| 706 } | |
| 707 | |
| 708 return this.load(); | |
| 709 }.bind(this)); | |
| 710 } | |
| 711 | |
| 712 this._setStatus("Some log streams could not be loaded."); | |
| 713 return null; | |
| 714 } | |
| 715 | |
| 716 // Install the new buffer and re-enter. | |
| 717 this._currentLogBuffer = buf; | |
| 718 return this.load(); | |
| 719 }.bind(this)).catch(function(error) { | |
| 720 this._setStatus("[" + error.name + "] fetching streams: " + | |
| 721 error.message); | |
| 722 throw error; | |
| 723 }.bind(this)); | |
| 724 }; | |
| 725 | |
| 726 /** | |
| 727 * Manages an aggregate log stream buffer, consisting of logs punted from a | |
| 728 * set of zero or more _BufferedStream instances. | |
| 729 */ | |
| 730 function _LogStreamBuffer() { | |
| 731 this._streams = null; | |
| 732 this._active = null; | |
| 733 this._nextBufferPromise = null; | |
| 734 this._failures = []; | |
| 735 | |
| 736 this.finished = false; | |
| 737 this._resetIterativeState(); | |
| 738 } | |
| 739 | |
| 740 _LogStreamBuffer.prototype.setStreams = function(streams) { | |
| 741 // TODO(dnj): Make this do a delta with previous streams so we don't lose | |
| 742 // their already-loaded logs if the page changes. | |
| 743 this._streams = streams.map(function(bs, i) { | |
| 744 return { | |
| 745 bs: bs, | |
| 746 active: true, | |
| 747 buffer: new _BufferedLogs(), | |
| 748 }; | |
| 749 }); | |
| 750 this._active = this._streams; | |
| 751 this._nextBufferPromise = null; | |
| 752 }; | |
| 753 | |
| 754 _LogStreamBuffer.prototype._resetIterativeState = function() { | |
| 755 this.autoRetry = false; | |
| 756 }; | |
| 757 | |
| 758 /** | |
| 759 * Returns a Promise that resolves into a _BufferedLogs instance containing | |
| 760 * the next set of logs, in order, from the source log streams. | |
| 761 * | |
| 762 * The _BufferedLogs bundle may have status flags set, and should be checked. | |
| 763 * | |
| 764 * The Promise will also resolve to "null" if there are no more logs in the | |
| 765 * source streams. | |
| 766 * | |
| 767 * If there are errors fetching logs, the Promise will be rejected, and an | |
| 768 * error will be returned. | |
| 769 */ | |
| 770 _LogStreamBuffer.prototype.nextBuffer = function() { | |
| 771 // If we're already are fetching the next buffer, return that Promise. | |
| 772 if (this._nextBufferPromise) { | |
| 773 return this._nextBufferPromise; | |
| 774 } | |
| 775 | |
| 776 // Filter our any finished streams from our active list. A stream is | |
| 777 // finished if it is finished streaming and we don't have a retained buffer | |
| 778 // from it. | |
| 779 this._active = this._active.filter(function(entry) { | |
| 780 return (entry.buffer.peek() || (! (entry.bs.finished || entry.bs.error))); | |
| 781 }) | |
| 782 | |
| 783 if (! this._active.length) { | |
| 784 this.finished = true; | |
| 785 } | |
| 786 if (this.finished) { | |
| 787 // No active streams, so we're finished. Permanently set our promise to | |
| 788 // the finished state. | |
| 789 this._nextBufferPromise = Promise.resolve(null); | |
| 790 return this._nextBufferPromise; | |
| 791 } | |
| 792 | |
| 793 // Fill all buffers for all active streams. This may result in an RPC to | |
| 794 // load new buffer content for streams whose buffers are empty. | |
| 795 // | |
| 796 // RPC failures are handled here: | |
| 797 // - If the stream reports "not found", we will terminate early and set | |
| 798 // out status to "waiting". Our owner should retry after a delay. | |
| 799 // - Otherwise, we will set our status to "error". Our owner should report | |
| 800 // that an error has occurred while loading stream data. | |
| 801 this._resetIterativeState(); | |
| 802 | |
| 803 var incomplete = false; | |
| 804 this._nextBufferPromise = Promise.all(this._active.map(function(entry) { | |
| 805 // If the entry's buffer still has data, use it immediately. | |
| 806 if (entry.buffer.peek()) { | |
| 807 return entry.buffer; | |
| 808 } | |
| 809 | |
| 810 // Get the next log buffer for each stream. This may result in an RPC. | |
| 811 return entry.bs.nextBuffer().then(function(buffer) { | |
| 812 // Retain this buffer, if valid. The stream may have returned a null | |
| 813 // buffer if it failed to fetch for a legitimate reason. In this case, | |
| 814 // we will not retain it (since we want entry.buffer to be valid), but | |
| 815 // will forward the "null" to our aggregate function. | |
| 816 if (buffer) { | |
| 817 entry.buffer = buffer; | |
| 818 } else { | |
| 819 incomplete = true; | |
| 820 | |
| 821 // If this stream is waiting, but not on auth, mark that we should | |
| 822 // automatically retry. | |
| 823 if (entry.bs.waiting && !entry.bs.auth) { | |
| 824 this.autoRetry = true; | |
| 825 } | |
| 826 } | |
| 827 return buffer; | |
| 828 }.bind(this)).catch(function(error) { | |
| 829 // Log stream source of error. Raise a generic "failed to buffer" | |
| 830 // error. This will become a permanent failure. | |
| 831 console.error("Error loading buffer for", entry.bs.stream.fullName(), | |
| 832 "(", entry.bs, "): ", error); | |
| 833 this._failures.push(entry.bs); | |
| 834 return null; | |
| 835 }.bind(this)); | |
| 836 }.bind(this))).then(function(buffers) { | |
| 837 this._nextBufferPromise = null; | |
| 838 | |
| 839 // Check each buffer. If any are null, that stream failed to deliver. | |
| 840 if (incomplete) { | |
| 841 // We succeeded, but are incomplete. At least one stream failed to | |
| 842 // deliver and should have state flags set accordingly. | |
| 843 return null; | |
| 844 } | |
| 845 | |
| 846 // Remove any null buffers. These would be placed here when a stream fails | |
| 847 // to load. Aggregate as much data from each of our streams as possible. | |
| 848 buffers = buffers.filter(v => (!!v)); | |
| 849 return this._aggregateBuffers(buffers); | |
| 850 }.bind(this)); | |
| 851 return this._nextBufferPromise; | |
| 852 }; | |
| 853 | |
| 854 _LogStreamBuffer.prototype._aggregateBuffers = function(buffers) { | |
| 855 switch (buffers.length) { | |
| 856 case 0: | |
| 857 // No buffers, so no logs. | |
| 858 return new _BufferedLogs(null); | |
| 859 case 1: | |
| 860 // As a special case, if we only have one buffer, and we assume that its | |
| 861 // entries are sorted, then that buffer is a return value. | |
| 862 return new _BufferedLogs(buffers[0].getAll()); | |
| 863 } | |
| 864 | |
| 865 // Preload our peek array. | |
| 866 var incomplete = false; | |
| 867 var peek = buffers.map(function(buf) { | |
| 868 var le = buf.peek(); | |
| 869 if (! le) { | |
| 870 incomplete = true; | |
| 871 } | |
| 872 return le; | |
| 873 }); | |
| 874 if (incomplete) { | |
| 875 // One of our input buffers had no log entries. | |
| 876 return new _BufferedLogs(null); | |
| 877 } | |
| 878 | |
| 879 // Assemble our aggregate buffer array. | |
| 880 // TODO: A binary heap would be pretty great for this. | |
| 881 var entries = []; | |
| 882 while (true) { | |
| 883 // Choose the next stream. | |
| 884 var earliest = 0; | |
| 885 for (var i = 1; i < buffers.length; i++) { | |
| 886 if (_LogStreamBuffer.compareLogs(peek[i], peek[earliest]) < 0) { | |
| 887 earliest = i; | |
| 888 } | |
| 889 } | |
| 890 | |
| 891 // Get the next log from the earliest stream. | |
| 892 entries.push(buffers[earliest].next()); | |
| 893 | |
| 894 // Repopulate that buffer's "peek" value. If the buffer has no more | |
| 895 // entries, then we're done. | |
| 896 var next = buffers[earliest].peek(); | |
| 897 if (!next) { | |
| 898 return new _BufferedLogs(entries); | |
| 899 } | |
| 900 peek[earliest] = next; | |
| 901 } | |
| 902 }; | |
| 903 | |
| 904 _LogStreamBuffer.compareLogs = function(a, b) { | |
| 905 // If they are part of the same stream, compare prefix indexes. | |
| 906 if (a.source.stream.samePrefixAs(b.source.stream)) { | |
| 907 return (a.prefixIndex - b.prefixIndex); | |
| 908 } | |
| 909 | |
| 910 // Compare based on timestamp. | |
| 911 return a.timestamp - b.timestamp; | |
| 912 }; | |
| 913 | |
| 914 | |
| 915 /** | |
| 916 * A buffer of ordered log entries from all streams. | |
| 917 * | |
| 918 * Assumes total ownership of the input log buffer, which can be null to | |
| 919 * indicate no logs. | |
| 920 */ | |
| 921 function _BufferedLogs(logs) { | |
| 922 this._logs = logs; | |
| 923 this._index = 0; | |
| 924 } | |
| 925 | |
| 926 _BufferedLogs.prototype.getAll = function() { | |
| 927 // Pop all logs. | |
| 928 var logs = this._logs; | |
| 929 this._logs = null; | |
| 930 return logs; | |
| 931 }; | |
| 932 | |
| 933 _BufferedLogs.prototype.peek = function() { | |
| 934 return (this._logs) ? (this._logs[this._index]) : (null); | |
| 935 }; | |
| 936 | |
| 937 _BufferedLogs.prototype.next = function() { | |
| 938 if (! (this._logs && this._logs.length)) { | |
| 939 return null; | |
| 940 } | |
| 941 | |
| 942 // Get the next log and increment our index. | |
| 943 var log = this._logs[this._index++]; | |
| 944 if (this._index >= this._logs.length) { | |
| 945 this._logs = null; | |
| 946 } | |
| 947 return log; | |
| 948 }; | |
| 949 | |
| 950 | |
| 951 /** | |
| 952 * Stateful log fetching manager for a single log stream. | |
| 953 */ | |
| 954 function _BufferedStream(stream, client, oneOfMany, statusCallback) { | |
| 955 this.stream = stream; | |
| 956 | |
| 957 this.error = null; | |
| 958 this.finished = false; | |
| 959 | |
| 960 this._fetcher = new LogDogFetcher(client, stream); | |
| 961 this._oneOfMany = oneOfMany; | |
| 962 this._statusCallback = statusCallback; | |
| 963 this._lastFetchIndex = null; | |
| 964 } | |
| 965 | |
| 966 _BufferedStream.INITIAL_FETCH_SIZE = 4096; | |
| 967 | |
| 968 _BufferedStream.prototype._resetIterativeState = function() { | |
| 969 this.waiting = false; | |
| 970 this.auth = false; | |
| 971 this._fireStatusUpdated(); | |
| 972 | |
| 973 this._currentFetch = null; | |
| 974 }; | |
| 975 | |
| 976 _BufferedStream.prototype.nextBuffer = function() { | |
| 977 if (this._currentFetch) { | |
| 978 return this._currentFetch; | |
| 979 } | |
| 980 | |
| 981 // Reset per-round state and begin next round fetch. | |
| 982 this._resetIterativeState(); | |
| 983 | |
| 984 // If this is the first fetch, and we're not the only log stream being | |
| 985 // rendered, fetch a small amount so we can (probably) start rendering | |
| 986 // without waiting for a lot of huge chunks. | |
| 987 this._fetcher.byteCount = ( | |
| 988 (this._lastFetchIndex === null) && this._oneOfMany) ? | |
| 989 (_BufferedStream.INITIAL_FETCH_SIZE) : (null); | |
| 990 | |
| 991 this._currentFetch = this._fetcher.next().then(function(result) { | |
| 992 this._currentFetch = null; | |
| 993 | |
| 994 // Update our stream information. | |
| 995 this.finished = this._fetcher.finished; | |
| 996 | |
| 997 // Augment each returned log entry with self-descriptive metadata. | |
| 998 var logs = result.entries; | |
| 999 if (logs && logs.length) { | |
| 1000 logs.forEach(function(le) { | |
| 1001 le.desc = result.desc; | |
| 1002 le.state = result.state; | |
| 1003 le.source = this; | |
| 1004 }.bind(this)); | |
| 1005 | |
| 1006 // Record the latest fetch index. | |
| 1007 this._lastFetchIndex = logs[logs.length - 1].streamIndex; | |
| 1008 } | |
| 1009 | |
| 1010 this._fireStatusUpdated(); | |
| 1011 return new _BufferedLogs(logs); | |
| 1012 }.bind(this)).catch(function(error) {; | |
| 1013 // If this is a "not found" error, we assume that the stream is valid, but | |
| 1014 // hasn't been ingested into LogDog yet. Return "null". | |
| 1015 if (error instanceof LogDogError) { | |
| 1016 if (error.isUnauthenticated()) { | |
| 1017 this.waiting = true; | |
| 1018 this.auth = true; | |
| 1019 } else if (error.isNotFound()) { | |
| 1020 this.waiting = true; | |
| 1021 } | |
| 1022 | |
| 1023 // If this is an error that we understand, recover from it, return | |
| 1024 // null, and set our status flags. | |
| 1025 if (this.waiting) { | |
| 1026 // Recover from this error. | |
| 1027 this._currentFetch = null; | |
| 1028 this._fireStatusUpdated(); | |
| 1029 return null; | |
| 1030 } | |
| 1031 } | |
| 1032 | |
| 1033 // Retain this error forever. | |
| 1034 this.error = error; | |
| 1035 throw error; | |
| 1036 }.bind(this)); | |
| 1037 return this._currentFetch; | |
| 1038 }; | |
| 1039 | |
| 1040 _BufferedStream.prototype._fireStatusUpdated = function() { | |
| 1041 if (this._statusCallback) { | |
| 1042 this._statusCallback(this); | |
| 1043 } | |
| 1044 }; | |
| 1045 | |
| 1046 _BufferedStream.prototype.description = function() { | |
| 1047 if (this._waiting) { | |
| 1048 return "(Waiting)"; | |
| 1049 } | |
| 1050 | |
| 1051 var pieces = [] | |
| 1052 var tidx = this._fetcher.terminalIndex(); | |
| 1053 if (this._lastFetchIndex) { | |
| 1054 if (tidx >= 0) { | |
| 1055 pieces.push(this._lastFetchIndex + " / " + tidx); | |
| 1056 } else { | |
| 1057 pieces.push(this._lastFetchIndex + "..."); | |
| 1058 } | |
| 1059 } | |
| 1060 | |
| 1061 if (this.error) { | |
| 1062 pieces.push("(Error)"); | |
| 1063 } else if (this.auth) { | |
| 1064 pieces.push("(Auth Error)"); | |
| 1065 } else if (this.waiting) { | |
| 1066 pieces.push("(Waiting)"); | |
| 1067 } else if (!this._fetcher.state) { | |
| 1068 pieces.push("(Fetching)"); | |
| 1069 } else if (this._fetcher.finished) { | |
| 1070 pieces.push("(Finished)"); | |
| 1071 } else { | |
| 1072 pieces.push("(Streaming)"); | |
| 1073 } | |
| 1074 return pieces.join(" "); | |
| 1075 }; | |
| 1076 </script> | 951 </script> |
| OLD | NEW |