| 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/iron-icons/iron-icons.html"> | 9 <link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html"
> |
| 10 <link rel="import" href="../bower_components/iron-icons/av-icons.html"> | 10 |
| 11 <link rel="import" href="../bower_components/iron-icons/editor-icons.html"> | 11 <link rel="import" href="../logdog-stream/logdog-stream.html"> |
| 12 <link rel="import" href="../bower_components/paper-button/paper-button.html"> | 12 <link rel="import" href="../logdog-stream/logdog-error.html"> |
| 13 <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button
.html"> | 13 <link rel="import" href="../luci-sleep-promise/luci-sleep-promise.html"> |
| 14 <link rel="import" href=""> | 14 <link rel="import" href="logdog-stream-fetcher.html"> |
| 15 <link rel="import" href="logdog-stream-query.html"> |
| 15 | 16 |
| 16 <!-- | 17 <!-- |
| 17 An element for rendering muxed LogDog log streams. | 18 An element for rendering muxed LogDog log streams. |
| 18 --> | 19 --> |
| 19 <dom-module id="logdog-stream-view"> | 20 <dom-module id="logdog-stream-view"> |
| 20 | 21 |
| 21 <template> | 22 <template> |
| 22 <style is="custom-style"> | 23 <style> |
| 23 #mainView { | 24 .buttons { |
| 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 { | |
| 35 background-color: white; | 25 background-color: white; |
| 36 } | 26 } |
| 37 | 27 |
| 38 .paper-button-highlight[toggles][active] { | 28 #stream-status { |
| 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 { | |
| 48 position: fixed; | 29 position: fixed; |
| 49 right: 16px; | 30 right: 16px; |
| 50 background-color: #EEEEEE; | 31 background-color: #EEEEEE; |
| 51 opacity: 0.7; | 32 opacity: 0.7; |
| 52 } | 33 } |
| 53 | 34 |
| 54 #logContent { | 35 #logContent { |
| 55 padding-top: 54px; /* Pad around buttons */ | 36 padding-top: 20px; |
| 56 background-color: white; | 37 background-color: white; |
| 57 } | 38 } |
| 58 | 39 |
| 59 .log-entry { | 40 .log-entry { |
| 60 padding: 0 0 0 0; | 41 padding: 0 0 0 0; |
| 61 clear: left; | 42 clear: left; |
| 62 } | 43 } |
| 63 | 44 |
| 64 .log-entry-meta { | 45 .log-entry-meta { |
| 65 background-color: lightgray; | 46 vertical-align: top; |
| 66 padding: 5px; | 47 padding: 0 8px 0 0; |
| 67 border-width: 2px 0px 0px 0px; | 48 margin: 0 0 0 0; |
| 68 border-color: darkgray; | 49 float: left; |
| 69 border-style: dotted; | |
| 70 user-select: none; | |
| 71 | |
| 72 font-style: italic; | 50 font-style: italic; |
| 73 font-family: Courier New, Courier, monospace; | 51 font-family: Courier New, Courier, monospace; |
| 74 font-size: 10px; | 52 font-size: 10px; |
| 75 | 53 |
| 76 /* Can be toggled to "flex" by applying .showMeta class to #logs. */ | 54 /* Fixed width, break word if necessary. */ |
| 55 width: 150px; |
| 56 word-break: break-word; |
| 57 |
| 58 /* Can be toggled by applying .showMeta class to #logs. */ |
| 77 display: none; | 59 display: none; |
| 78 } | 60 } |
| 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 } | |
| 88 .showMeta .log-entry-meta { | 61 .showMeta .log-entry-meta { |
| 89 display: flex; | 62 display: block; |
| 90 } | 63 } |
| 91 | 64 |
| 92 /* .log-entry-content { */ | 65 .log-entry-content { |
| 93 .log-entry-chunk { | |
| 94 padding: 0 0 0 0; | 66 padding: 0 0 0 0; |
| 95 margin: 0 0 0 0; | 67 margin: 0 0 0 0; |
| 96 float: none; | 68 float: none; |
| 97 font-family: Courier New, Courier, monospace; | 69 font-family: Courier New, Courier, monospace; |
| 98 font-size: 16px; | 70 font-size: 16px; |
| 99 list-style: none; | 71 list-style: none; |
| 72 } |
| 100 | 73 |
| 101 border-bottom: 1px solid #CCCCCC; | 74 .log-entry-line { |
| 75 padding-left: 0; |
| 102 | 76 |
| 103 /* Can be toggled by applying .wrapLines class to #logs. */ | 77 /* Can be toggled by applying .wrapLines class to #logs. */ |
| 104 white-space: pre; | 78 white-space: pre; |
| 105 } | 79 } |
| 106 | 80 .wrapLines .log-entry-line { |
| 107 /*.wrapLines .log-entry-content { */ | |
| 108 .wrapLines .log-entry-chunk { | |
| 109 white-space: pre-wrap; | 81 white-space: pre-wrap; |
| 110 word-break: break-word; | 82 word-break: break-word; |
| 111 } | 83 } |
| 112 | 84 |
| 113 .logFetchButtonContainer { | 85 .log-entry-line:nth-last-child(2) { |
| 114 height: auto; | 86 border-bottom: 1px solid #CCCCCC; |
| 115 display: none; | |
| 116 flex-direction: row; | |
| 117 background-color: rgba(0, 0, 0, 0.2); | |
| 118 padding: 2px; | |
| 119 } | 87 } |
| 120 | 88 |
| 121 .logFetchButtonVisible { | 89 #bottom { |
| 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 { | |
| 138 background-color: lightcoral; | 90 background-color: lightcoral; |
| 139 } | 91 height: 2px; |
| 140 | 92 margin-bottom: 10px; |
| 141 #logEnd { | |
| 142 margin-bottom: 30px; | |
| 143 background-color: gray; | |
| 144 } | |
| 145 | |
| 146 .clickable-log-anchor { | |
| 147 height: 24px; | |
| 148 } | 93 } |
| 149 | 94 |
| 150 #status-bar { | 95 #status-bar { |
| 151 /* Overlay at the bottom of the page. */ | 96 /* Overlay at the bottom of the page. */ |
| 152 position: fixed; | 97 position: fixed; |
| 153 z-index: 9999; | 98 z-index: 9999; |
| 154 overflow: hidden; | 99 overflow: hidden; |
| 155 bottom: 0; | 100 bottom: 0; |
| 156 left: 0; | 101 left: 0; |
| 157 width: 100%; | 102 width: 100%; |
| 158 | 103 |
| 159 text-align: center; | 104 text-align: center; |
| 160 font-size: 16px; | 105 font-size: 16px; |
| 161 background-color: rgba(245, 245, 220, 0.7); | 106 background-color: rgba(245, 245, 220, 0.7); |
| 162 } | 107 } |
| 163 </style> | 108 </style> |
| 164 | 109 |
| 110 <google-signin-aware |
| 111 id="aware" |
| 112 on-google-signin-aware-success="_onSignin"></google-signin-aware> |
| 113 |
| 165 <rpc-client | 114 <rpc-client |
| 166 id="client" | 115 id="client" |
| 167 auto-token | 116 auto-token |
| 168 host="[[host]]"></rpc-client> | 117 host="[[host]]"></rpc-client> |
| 169 | 118 |
| 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 | |
| 178 <!-- Stream view options. --> | 119 <!-- Stream view options. --> |
| 179 <div id="mainView"> | 120 <div class="main-view"> |
| 180 <div id="buttons"> | 121 <div class="buttons"> |
| 181 <!-- If we have exactly one stream, we will enable users to split. --> | 122 <paper-checkbox checked="{{showMetadata}}"> |
| 182 <template is="dom-if" if="{{showStreamingControls}}"> | 123 Show Metadata |
| 183 <template is="dom-if" if="{{canSplit}}"> | 124 </paper-checkbox> |
| 184 <paper-icon-button title="Split, load logs from end" | 125 <paper-checkbox checked="{{wrapLines}}"> |
| 185 icon="editor:vertical-align-bottom" | 126 Wrap Lines |
| 186 on-tap="_splitClicked"> | 127 </paper-checkbox> |
| 187 </paper-icon-button> | 128 <paper-checkbox checked="{{follow}}"> |
| 188 </template> | 129 Follow |
| 189 | 130 </paper-checkbox> |
| 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> | |
| 228 </div> | 131 </div> |
| 229 | 132 |
| 230 <!-- Display current fetching status, if stream data is still loading. --> | 133 <!-- Display current fetching status, if stream data is still loading. --> |
| 231 <div id="streamStatus"> | 134 <template is="dom-if" if="{{streamStatus}}"> |
| 232 <template is="dom-if" if="{{streamStatus}}"> | 135 <div id="stream-status"> |
| 233 <table> | 136 <table> |
| 234 <template is="dom-repeat" items="{{streamStatus}}"> | 137 <template is="dom-repeat" items="{{streamStatus}}"> |
| 235 <tr> | 138 <tr> |
| 236 <td>{{item.name}}</td> | 139 <td>{{item.name}}</td> |
| 237 <td>{{item.desc}}</td> | 140 <td>{{item.desc}}</td> |
| 238 </tr> | 141 </tr> |
| 239 </template> | 142 </template> |
| 240 </table> | 143 </table> |
| 241 </template> | 144 </div> |
| 242 </div> | 145 </template> |
| 243 | 146 |
| 244 <!-- Muxed log content. --> | 147 <!-- Muxed log content. --> |
| 245 <div id="logContent" | 148 <div id="logContent" on-mousewheel="_handleMouseWheel"> |
| 246 on-mousewheel="_handleMouseWheel"> | |
| 247 <div id="logs"> | 149 <div id="logs"> |
| 248 <!-- Content will be populated with JavaScript as logs are loaded. | 150 <!-- Content will be populated with JavaScript as logs are loaded. |
| 249 | 151 |
| 250 <div class="log-entry"> | 152 <div class="log-entry"> |
| 251 <div class="log-entry-meta"> | 153 <div class="log-entry-meta"> |
| 252 <div class="log-entry-meta-line">(Meta 0)</div> | 154 <div class="log-entry-meta-line">(Meta 0)</div> |
| 253 ... | 155 ... |
| 254 <div class="log-entry-meta-line">(Meta N)</div> | 156 <div class="log-entry-meta-line">(Meta N)</div> |
| 255 </div> | 157 </div> |
| 256 <div class="log-entry-content"> | 158 <div class="log-entry-content"> |
| 257 LINE #0 | 159 <div class="log-entry-line">LINE #0</div> |
| 258 ... | 160 ... |
| 259 LINE #N | 161 <div class="log-entry-line">LINE #N</div> |
| 260 </div> | 162 </div> |
| 261 </div> | 163 </div> |
| 262 ... | 164 ... |
| 263 | 165 |
| 166 --> |
| 167 </div> |
| 264 | 168 |
| 265 Note that we can't use templating to show/hide the log dividers, | 169 <!-- Current red bottom line. --> |
| 266 since our positional log insertion requires them to be present and | 170 <div id="bottom"></div> |
| 267 move along with insertions as points of reference. | |
| 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> | |
| 299 </div> | |
| 300 </div> | 171 </div> |
| 301 | 172 |
| 302 </div> | 173 </div> |
| 303 | 174 |
| 304 <template is="dom-if" if="{{statusBar}}"> | 175 <template is="dom-if" if="{{statusBar}}"> |
| 305 <div id="status-bar">{{statusBar.value}}</div> | 176 <div id="status-bar">{{statusBar.value}}</div> |
| 306 </template> | 177 </template> |
| 307 </template> | 178 </template> |
| 308 | 179 |
| 309 </dom-module> | 180 </dom-module> |
| (...skipping 18 matching lines...) Expand all Loading... |
| 328 * project. For example, for stream "foo/bar/+/baz" in project "chromium", | 199 * project. For example, for stream "foo/bar/+/baz" in project "chromium", |
| 329 * the stream path would be: "chromium/foo/bar/+/baz". | 200 * the stream path would be: "chromium/foo/bar/+/baz". |
| 330 */ | 201 */ |
| 331 streams: { | 202 streams: { |
| 332 type: Array, | 203 type: Array, |
| 333 value: [], | 204 value: [], |
| 334 notify: true, | 205 notify: true, |
| 335 observer: "_streamsChanged", | 206 observer: "_streamsChanged", |
| 336 }, | 207 }, |
| 337 | 208 |
| 338 toolbarAnchor: { | |
| 339 type: Object, | |
| 340 value: null, | |
| 341 }, | |
| 342 | |
| 343 mobile: { | |
| 344 type: Boolean, | |
| 345 value: false, | |
| 346 }, | |
| 347 | |
| 348 /** | 209 /** |
| 349 * The number of logs to load before forcing a page refresh. | 210 * The number of logs to load before forcing a page refresh. |
| 350 * | 211 * |
| 351 * The smaller the value, the smoother the page will behave while logs are | 212 * The smaller the value, the smoother the page will behave while logs are |
| 352 * loading. However, the logs will also load slower because of forced | 213 * loading. However, the logs will also load slower because of forced |
| 353 * renders in between elements. | 214 * renders in between elements. |
| 354 */ | 215 */ |
| 355 burst: { | 216 burst: { |
| 356 type: Number, | 217 type: Number, |
| 357 value: 1000, | 218 value: 1000, |
| 358 notify: true, | 219 notify: true, |
| 359 }, | 220 }, |
| 360 | 221 |
| 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 | |
| 373 /** If true, show log metadata column. */ | 222 /** If true, show log metadata column. */ |
| 374 showMetadata: { | 223 showMetadata: { |
| 375 type: Boolean, | 224 type: Boolean, |
| 376 value: false, | 225 value: false, |
| 377 observer: "_showMetadataChanged", | 226 observer: "_showMetadataChanged", |
| 378 }, | 227 }, |
| 379 | 228 |
| 380 /** If true, wrap log lines to the screen. */ | 229 /** If true, wrap log lines to the screen. */ |
| 381 wrapLines: { | 230 wrapLines: { |
| 382 type: Boolean, | 231 type: Boolean, |
| 383 value: false, | 232 value: false, |
| 384 observer: "_wrapLinesChanged", | 233 observer: "_wrapLinesChanged", |
| 385 }, | 234 }, |
| 386 | 235 |
| 387 /** | 236 /** |
| 388 * If true, automatically scroll the page to the bottom of the logs | 237 * If true, automatically scroll the page to the bottom of the logs |
| 389 * while they are streaming. | 238 * while they are streaming. |
| 390 */ | 239 */ |
| 391 follow: { | 240 follow: { |
| 392 type: Boolean, | 241 type: Boolean, |
| 393 value: false, | 242 value: false, |
| 394 observer: "_followChanged", | 243 observer: "_followChanged", |
| 395 }, | 244 }, |
| 396 | 245 |
| 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 | |
| 443 /** | 246 /** |
| 444 * The current stream status. This is an Array of objects: | 247 * The current stream status. This is an Array of objects: |
| 445 * obj.name is the name of the stream. | 248 * obj.name is the name of the stream. |
| 446 * obj.desc is the status description of the stream. | 249 * obj.desc is the status description of the stream. |
| 447 */ | 250 */ |
| 448 streamStatus: { | 251 streamStatus: { |
| 449 type: String, | 252 type: String, |
| 450 value: null, | 253 value: null, |
| 451 notify: true, | 254 notify: true, |
| 452 readOnly: true, | 255 readOnly: true, |
| 453 }, | 256 }, |
| 454 | 257 |
| 455 /** | 258 /** |
| 456 * The text content of the status element at the bottom of the page. | 259 * The text content of the status element at the bottom of the page. |
| 457 */ | 260 */ |
| 458 statusBar: { | 261 statusBar: { |
| 459 type: String, | 262 type: String, |
| 460 value: null, | 263 value: null, |
| 461 readOnly: true, | 264 readOnly: true, |
| 462 }, | 265 }, |
| 463 }, | 266 }, |
| 464 | 267 |
| 465 created: function() { | 268 ready: function() { |
| 466 this._scrollTimeoutId = null; | 269 this._scheduledWrite = null; |
| 467 | 270 this._buffer = null; |
| 468 // Create "onScrollHandler", which just invokes "_onScroll" while bound | 271 this._currentLogBuffer = null; |
| 469 // to "this". We create it here so we can unregister it later, since | 272 this._authCallback = null; |
| 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); | |
| 492 }, | 273 }, |
| 493 | 274 |
| 494 detached: function() { | 275 detached: function() { |
| 495 // Unregsiter event handlers. | 276 this.stop(); |
| 496 window.removeEventListener('scroll', this._onScrollHandler); | |
| 497 | |
| 498 // Reset state. | |
| 499 this.reset(); | |
| 500 }, | 277 }, |
| 501 | 278 |
| 502 stop: function() { | 279 stop: function() { |
| 503 this.reset(); | 280 this._cancelFetch(true); |
| 504 }, | 281 }, |
| 505 | 282 |
| 506 /** Clears state and begins fetching log data. */ | 283 /** Clears state and begins fetching log data. */ |
| 507 reset: function() { | 284 reset: function() { |
| 508 if ( this._model ) { | 285 this._resetLogState(); |
| 509 this._model.reset(); | 286 |
| 510 } | 287 this._resolveStreams().then(function(streams) { |
| 511 this._resetScroll(); | 288 this._resetToStreams(streams); |
| 512 this._model = null; | 289 }.bind(this)).catch(function(error) { |
| 513 this._renderedLogs = false; | 290 this._loadStatusBar("Failed to resolve streams:" + error); |
| 291 throw error; |
| 292 }.bind(this)); |
| 293 }, |
| 294 |
| 295 /** Clears all current logs. */ |
| 296 _resetLogState: function() { |
| 297 this._cancelFetch(true); |
| 298 |
| 299 // Remove all current log elements. */ |
| 300 while (this.$.logs.hasChildNodes()) { |
| 301 this.$.logs.removeChild(this.$.logs.lastChild); |
| 302 } |
| 303 |
| 304 // Clear our buffer and streamer state. |
| 305 this._buffer = null; |
| 306 this._currentLogBuffer = null; |
| 307 if (this._streamer) { |
| 308 this._streamer.shutdown(); |
| 309 } |
| 310 this._streamer = null; |
| 311 }, |
| 312 |
| 313 _resolveStreams: function() { |
| 314 // Separate our configured streams into full stream paths and queries. |
| 315 var parts = { |
| 316 queries: [], |
| 317 streams: [], |
| 318 }; |
| 319 var query = new LogDogQuery(this.project); |
| 320 this.streams.map(LogDogStream.splitProject).forEach(function(v) { |
| 321 if (LogDogQuery.isQuery(v.path)) { |
| 322 parts.queries.push(v); |
| 323 } else { |
| 324 parts.streams.push(v); |
| 325 } |
| 326 }); |
| 327 |
| 328 // Resolve any outstanding queries into full stream paths. |
| 329 // |
| 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 } |
| 514 }, | 456 }, |
| 515 | 457 |
| 516 /** | 458 /** |
| 517 * Called each time a scroll event is fired. Since this can be really | 459 * This is an iterative function that grabs the next set of logs and renders |
| 518 * frequent, this will kick off a "scroll handler" in the background at an | 460 * them. Afterwards, it will continue rescheduling itself until there are |
| 519 * interval. Multiple scroll events within that interval will only result | 461 * no more logs to render. |
| 520 * in one scroll handler invocation. | |
| 521 */ | 462 */ |
| 522 _onScroll: function(e) { | 463 _writeNextLogs: function() { |
| 523 if ( this._scrollTimeoutId ) { | 464 this._cancelScheduledWrite(); |
| 524 return; | 465 |
| 525 } | 466 this._streamer.load().then(function(entries) { |
| 526 | 467 // If there are no entries, then we're done. |
| 527 window.setTimeout(function() { | 468 if (! entries) { |
| 528 this._handleScrollEvent(e); | 469 // Cancel all fetching state. If our streamer is finished, also clear |
| 529 }.bind(this), 100); | 470 // messages and status. |
| 530 }, | 471 if (this._streamer.finished) { |
| 531 | 472 if (this._streamer.someStreamsFailed) { |
| 532 /** Actual scroll event handler. */ | 473 this._cancelFetch(false); |
| 533 _handleScrollEvent: function(e) { | 474 this._loadStatusBar("Some streams failed to load."); |
| 534 this._resetScroll(); | 475 } else { |
| 535 | 476 this._cancelFetch(true); |
| 536 // Update our button bar position to be relative to the parent's height. | 477 } |
| 537 this._adjustToTop(this.$.buttons); | 478 } else { |
| 538 this._adjustToTop(this.$.streamStatus); | 479 // No more logs, but also we are not finished. Retry after auth. |
| 539 }, | 480 this._authCallback = this._scheduleWriteNextLogs.bind(this); |
| 540 | 481 } |
| 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; | 482 return; |
| 627 } | 483 } |
| 628 | 484 |
| 629 // If we're rendering metadata, render an element per log entry. | 485 var logEntryChunk = document.createElement("div"); |
| 630 if( this.metadata ) { | 486 entries.forEach(function(le) { |
| 631 var entryRow = document.createElement("div"); | 487 this._appendLogEntry(logEntryChunk, le); |
| 632 entryRow.className = "log-entry"; | 488 }.bind(this)); |
| 633 | 489 |
| 634 // Metadata column. | 490 // To have styles apply correctly, we need to add it twice, see |
| 635 var metadataBlock = document.createElement("div"); | 491 // https://github.com/Polymer/polymer/issues/3100. |
| 636 metadataBlock.className = "log-entry-meta"; | 492 Polymer.dom(this.root).appendChild(logEntryChunk); |
| 637 | 493 this.$.logs.appendChild(logEntryChunk); |
| 638 this._appendMetaLine(metadataBlock, "Timestamp:", le.timestamp); | 494 |
| 639 this._appendMetaLine(metadataBlock, "Stream:", le.desc.name); | 495 // Yield so that our browser can refresh. We can't directly use |
| 640 this._appendMetaLine(metadataBlock, "Index:", le.streamIndex); | 496 // this.async since a timeout of "0" causes immediate execution instead |
| 641 | 497 // of yielding. |
| 642 // Log column. | 498 setTimeout(function() { |
| 643 var logDataBlock = document.createElement("div"); | 499 this._scheduleWriteNextLogs(); |
| 644 logDataBlock.className = "log-entry-content"; | 500 }.bind(this), 0); |
| 645 | 501 }.bind(this)); |
| 646 le.text.lines.forEach(function(line) { | 502 }, |
| 647 lines.push(line.value); | 503 |
| 648 }); | 504 _appendLogEntry: function(root, le) { |
| 649 | 505 var text = le.text; |
| 650 logDataBlock.textContent = lines.join("\n"); | 506 if (!(text && text.lines)) { |
| 651 lines.length = 0; | 507 return 0; |
| 652 | 508 } |
| 653 entryRow.appendChild(metadataBlock); | 509 |
| 654 entryRow.appendChild(logDataBlock); | 510 // Create elements manually to avoid Polymer overhead for large logs. |
| 655 | 511 var entryRow = document.createElement("div"); |
| 656 logEntryChunk.appendChild(entryRow); | 512 entryRow.className = "log-entry"; |
| 657 lastLogEntry = entryRow; | 513 |
| 658 } else { | 514 // Metadata column. |
| 659 // Add this to the lines. We'll assign this directly to logEntryChunk | 515 var metadataBlock = document.createElement("div"); |
| 660 // after the loop. | 516 metadataBlock.className = "log-entry-meta"; |
| 661 le.text.lines.forEach(function(line) { | 517 entryRow.appendChild(metadataBlock); |
| 662 lines.push(line.value); | 518 |
| 663 }); | 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); |
| 664 } | 543 } |
| 665 }.bind(this)); | 544 } |
| 666 | 545 entryRow.appendChild(logDataBlock); |
| 667 if ( ! this.metadata ) { | 546 root.appendChild(entryRow); |
| 668 // Only one HTML element: the chunk. | 547 |
| 669 logEntryChunk.textContent = lines.join("\n"); | 548 return le.text.lines.length; |
| 670 lastLogEntry = logEntryChunk; | 549 }, |
| 671 } | 550 |
| 672 | 551 _updateStreamStatus: function(bs, idx) { |
| 673 // To have styles apply correctly, we need to add it twice, see | 552 var origStatus = this.streamStatus[idx]; |
| 674 // https://github.com/Polymer/polymer/issues/3100. | 553 this.splice("streamStatus", idx, 1, { |
| 675 Polymer.dom(this.root).appendChild(logEntryChunk); | 554 name: origStatus.name, |
| 676 | 555 desc: bs.description(), |
| 677 // Add the log entry to the appropriate place. | 556 }); |
| 678 var anchor, scrollToTop = false; | 557 }, |
| 679 var forceScroll = false; | 558 |
| 680 switch ( insertion ) { | 559 /** Scrolls to the bottom if "follow" is enabled. */ |
| 681 case this._v.Location.HEAD: | 560 _maybeScrollToBottom: function() { |
| 682 // InsertionPoint.HEAD: PREPEND to "logSplit". | 561 if (this.follow) { |
| 683 this.$.logs.insertBefore(logEntryChunk, this.$.logSplit); | 562 this.$.bottom.scrollIntoView({ |
| 684 | 563 "behavior": "smooth", |
| 685 // If we're not split, scroll to the log bottom. Otherwise, scroll to | 564 "block": "end", |
| 686 // the split. | 565 }); |
| 687 anchor = lastLogEntry; | 566 } |
| 688 break; | 567 }, |
| 689 | 568 |
| 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() { | |
| 723 // Remove all current log elements. */ | |
| 724 for ( var cur = this.$.logs.firstChild; cur; ) { | |
| 725 var del = cur; | |
| 726 cur = cur.nextElementSibling; | |
| 727 if ( del.classList && del.classList.contains('log-entry-chunk') ) { | |
| 728 this.$.logs.removeChild(del); | |
| 729 } | |
| 730 } | |
| 731 }, | |
| 732 | |
| 733 _locationIsVisible: function(l) { | |
| 734 var anchor; | |
| 735 switch( l ) { | |
| 736 case this._v.Location.HEAD: | |
| 737 case this._v.Location.TAIL: | |
| 738 anchor = this.$.logSplit; | |
| 739 | |
| 740 case this._v.Location.BOTTOM: | |
| 741 anchor = this.$.logBottom; | |
| 742 | |
| 743 default: | |
| 744 return false; | |
| 745 } | |
| 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 }); | |
| 813 } else { | |
| 814 // Bug? "block: start" doesn't seem to work the same as false. | |
| 815 element.scrollIntoView(false); | |
| 816 } | |
| 817 } | |
| 818 }, | |
| 819 | |
| 820 /** | 569 /** |
| 821 * Callback when "showMetadata" has changed. This adds/removes the | 570 * Callback when "showMetadata" has changed. This adds/removes the |
| 822 * "showMeta" CSS class from the metadata column. | 571 * "showMeta" CSS class from the metadata column. |
| 823 */ | 572 */ |
| 824 _showMetadataChanged: function(v) { | 573 _showMetadataChanged: function(v) { |
| 825 this.toggleClass("showMeta", v, this.$.logs); | 574 this.toggleClass("showMeta", v, this.$.logs); |
| 826 }, | 575 }, |
| 827 | |
| 828 /** | 576 /** |
| 829 * Callback when "wrapLines" has changed. This adds/removes the | 577 * Callback when "wrapLines" has changed. This adds/removes the |
| 830 * "wrapLines" CSS class to the log data. | 578 * "wrapLines" CSS class to the log data. |
| 831 */ | 579 */ |
| 832 _wrapLinesChanged: function(v) { | 580 _wrapLinesChanged: function(v) { |
| 833 this.toggleClass("wrapLines", v, this.$.logs); | 581 this.toggleClass("wrapLines", v, this.$.logs); |
| 834 }, | 582 }, |
| 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) ? | |
| 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 | |
| 866 /** Callback when "follow" has changed. */ | 583 /** Callback when "follow" has changed. */ |
| 867 _followChanged: function(v) { | 584 _followChanged: function(v) { |
| 868 if ( ! this._model ) { | 585 this._maybeScrollToBottom(); |
| 869 return; | 586 }, |
| 870 } | |
| 871 | 587 |
| 872 if ( v ) { | 588 /** Callback for when the mouse wheel has scrolled. Disables follow. */ |
| 873 // If follow is toggled on, automatically begin playing. | 589 _handleMouseWheel: function() { |
| 874 this.playing = true; | 590 this.follow = false; |
| 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); | |
| 906 }, | 591 }, |
| 907 | 592 |
| 908 /** | 593 /** |
| 909 * Loads text content into the status bar. | 594 * Loads text content into the status bar. |
| 910 * | 595 * |
| 911 * If null is passed, the status bar will be cleared. If text is passed, the | 596 * If null is passed, the status bar will be cleared. If text is passed, the |
| 912 * status bar will become visible with the supplied content. | 597 * status bar will become visible with the supplied content. |
| 913 */ | 598 */ |
| 914 _loadStatusBar: function(v) { | 599 _loadStatusBar: function(v) { |
| 915 var st = null; | 600 var st = null; |
| 916 if (v) { | 601 if (v) { |
| 917 st = { | 602 st = { |
| 918 value: v, | 603 value: v, |
| 919 }; | 604 }; |
| 920 } | 605 } |
| 921 this._setStatusBar(st); | 606 this._setStatusBar(st); |
| 922 }, | 607 }, |
| 923 | 608 |
| 924 _onSignin: function() { | 609 _onSignin: function() { |
| 925 if ( this._model ) { | 610 var fn = this._authCallback; |
| 926 this._model.notifyAuthenticationChanged(); | 611 if (fn) { |
| 612 this._authCallback = null; |
| 613 fn(); |
| 927 } | 614 } |
| 928 }, | 615 }, |
| 929 | |
| 930 _elementInViewport: function(el) { | |
| 931 var top = el.offsetTop; | |
| 932 var left = el.offsetLeft; | |
| 933 var width = el.offsetWidth; | |
| 934 var height = el.offsetHeight; | |
| 935 | |
| 936 while(el.offsetParent) { | |
| 937 el = el.offsetParent; | |
| 938 top += el.offsetTop; | |
| 939 left += el.offsetLeft; | |
| 940 } | |
| 941 | |
| 942 return ( | |
| 943 top < (window.pageYOffset + window.innerHeight) && | |
| 944 left < (window.pageXOffset + window.innerWidth) && | |
| 945 (top + height) > window.pageYOffset && | |
| 946 (left + width) > window.pageXOffset | |
| 947 ); | |
| 948 }, | |
| 949 | |
| 950 }); | 616 }); |
| 617 |
| 618 /** |
| 619 * Continuously loads log streams from a _LogStreamBuffer and exposes them via |
| 620 * callback. |
| 621 */ |
| 622 function _LogStreamer(buffer, burst, statusCallback) { |
| 623 this._buffer = buffer; |
| 624 this._burst = (burst || 0); |
| 625 this._missingDelay = 5000; |
| 626 this._statusCallback = statusCallback; |
| 627 |
| 628 this.finished = false; |
| 629 this.someStreamsFailed = false; |
| 630 |
| 631 this._currentLogBuffer = null; |
| 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 } |
| 669 |
| 670 // Have we exhausted this buffer? |
| 671 if (! current.peek()) { |
| 672 this._currentLogBuffer = null; |
| 673 } |
| 674 |
| 675 // Return the bundle of entries. |
| 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 }; |
| 951 </script> | 1076 </script> |
| OLD | NEW |