| <!-- |
| Copyright 2016 The LUCI Authors. All rights reserved. |
| Use of this source code is governed under the Apache License, Version 2.0 |
| that can be found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="../bower_components/polymer/polymer.html"> |
| <link rel="import" href="../bower_components/google-signin/google-signin-aware.html"> |
| <link rel="import" href="../bower_components/iron-icons/iron-icons.html"> |
| <link rel="import" href="../bower_components/iron-icons/av-icons.html"> |
| <link rel="import" href="../bower_components/iron-icons/editor-icons.html"> |
| <link rel="import" href="../bower_components/paper-button/paper-button.html"> |
| <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html"> |
| |
| <!-- |
| An element for rendering muxed LogDog log streams. |
| --> |
| <dom-module id="logdog-stream-view"> |
| |
| <template> |
| <style is="custom-style"> |
| #mainView { |
| position: relative; |
| } |
| |
| #buttons { |
| position: fixed; |
| height: auto; |
| padding: 5px; |
| background-color: rgba(0, 0, 0, 0.1); |
| z-index: 100; |
| } |
| #buttons > paper-button { |
| background-color: white; |
| } |
| |
| .paper-button-highlight[toggles][active] { |
| background-color: #cd6a51; |
| } |
| |
| .paper-icon-button-highlight[toggles][active] { |
| background-color: #cd6a51; |
| border-radius: 80%; |
| } |
| |
| #streamStatus { |
| position: fixed; |
| right: 16px; |
| background-color: #EEEEEE; |
| opacity: 0.7; |
| } |
| |
| #logContent { |
| padding-top: 54px; /* Pad around buttons */ |
| background-color: white; |
| } |
| |
| .log-entry { |
| padding: 0 0 0 0; |
| clear: left; |
| } |
| |
| .log-entry-meta { |
| background-color: lightgray; |
| padding: 5px; |
| border-width: 2px 0px 0px 0px; |
| border-color: darkgray; |
| border-style: dotted; |
| user-select: none; |
| |
| font-style: italic; |
| font-family: Courier New, Courier, monospace; |
| font-size: 10px; |
| |
| /* Can be toggled to "flex" by applying .showMeta class to #logs. */ |
| display: none; |
| } |
| .log-entry-meta-line { |
| padding: 5px; |
| border-width: 1px; |
| border-style: solid; |
| border-color: gray; |
| border-radius: 10px; |
| margin-right: 10px; |
| text-align: center; |
| } |
| .showMeta .log-entry-meta { |
| display: flex; |
| } |
| |
| /* .log-entry-content { */ |
| .log-entry-chunk { |
| padding: 0 0 0 0; |
| margin: 0 0 0 0; |
| float: none; |
| font-family: Courier New, Courier, monospace; |
| font-size: 16px; |
| list-style: none; |
| |
| border-bottom: 1px solid #CCCCCC; |
| -webkit-font-smoothing: auto; |
| |
| /* Can be toggled by applying .wrapLines class to #logs. */ |
| white-space: pre; |
| } |
| |
| /*.wrapLines .log-entry-content { */ |
| .wrapLines .log-entry-chunk { |
| white-space: pre-wrap; |
| word-break: break-word; |
| } |
| |
| .logFetchButtonContainer { |
| height: auto; |
| display: none; |
| flex-direction: row; |
| background-color: rgba(0, 0, 0, 0.2); |
| padding: 2px; |
| } |
| |
| .logFetchButtonVisible { |
| display: flex !important; |
| } |
| |
| .logFetchButton { |
| width: 100%; |
| height: 18px; |
| } |
| |
| .logSplitUpButton { |
| background: yellow; |
| } |
| .logSplitDownButton { |
| background: green; |
| } |
| |
| .logBottomButton { |
| background-color: lightcoral; |
| } |
| |
| #logEnd { |
| margin-bottom: 30px; |
| background-color: gray; |
| } |
| |
| .clickable-log-anchor { |
| height: 24px; |
| } |
| |
| #status-bar { |
| /* Overlay at the bottom of the page. */ |
| position: fixed; |
| z-index: 9999; |
| overflow: hidden; |
| bottom: 0; |
| left: 0; |
| width: 100%; |
| user-select: none; |
| |
| text-align: center; |
| font-size: 16px; |
| background-color: rgba(245, 245, 220, 0.7); |
| } |
| </style> |
| |
| <rpc-client |
| id="client" |
| auto-token |
| host="[[host]]"></rpc-client> |
| |
| <!-- |
| This must be after "rpc-client" so we get the signin event after it |
| does. |
| --> |
| <google-signin-aware |
| id="aware" |
| on-google-signin-aware-success="_onSignin"></google-signin-aware> |
| |
| <!-- Stream view options. --> |
| <div id="mainView"> |
| <div id="buttons"> |
| <!-- If we have exactly one stream, we will enable users to split. --> |
| <template is="dom-if" if="{{showStreamingControls}}"> |
| <paper-icon-button class="paper-icon-button-highlight" toggles |
| title="Stick to bottom." icon="icons:update" |
| active="{{follow}}"> |
| </paper-icon-button> |
| |
| <paper-icon-button class="paper-icon-button-highlight" toggles |
| title="Auto-Load" icon="{{playingIconName}}" |
| active="{{playing}}"> |
| </paper-icon-button> |
| |
| <template is="dom-if" if="{{canSplit}}"> |
| <paper-icon-button |
| title="Jump to latest." |
| icon="editor:vertical-align-bottom" |
| on-tap="_splitClicked"> |
| </paper-icon-button> |
| </template> |
| |
| <template is="dom-if" if="{{isSplit}}"> |
| <paper-icon-button title="Scroll to split" |
| icon="editor:vertical-align-center" on-tap="_scrollToSplit"> |
| </paper-icon-button> |
| </template> |
| |
| <template is="dom-if" if="{{isSplit}}"> |
| <paper-icon-button toggles |
| title="Load new logs, or backfill from top." |
| icon="{{backfillIconName}}" |
| active="{{backfill}}"> |
| </paper-icon-button> |
| </template> |
| </template> |
| |
| <template is="dom-if" if="{{_not(playing)}}"> |
| <paper-button class="paper-button-highlight" toggles raised |
| active="{{wrapLines}}"> |
| Wrap |
| </paper-button> |
| |
| <template is="dom-if" if="{{metadata}}"> |
| <paper-button class="paper-button-highlight" toggles raised |
| active="{{showMetadata}}"> |
| Metadata |
| </paper-button> |
| </template> |
| </template> |
| </div> |
| |
| <!-- Display current fetching status, if stream data is still loading. --> |
| <div id="streamStatus"> |
| <template is="dom-if" if="{{streamStatus}}"> |
| <table> |
| <template is="dom-repeat" items="{{streamStatus}}"> |
| <tr> |
| <td>{{item.name}}</td> |
| <td>{{item.desc}}</td> |
| </tr> |
| </template> |
| </table> |
| </template> |
| </div> |
| |
| <!-- Muxed log content. --> |
| <div id="logContent" |
| on-mousewheel="_handleMouseWheel"> |
| <div id="logs"> |
| <!-- Content will be populated with JavaScript as logs are loaded. |
| |
| <div class="log-entry"> |
| <div class="log-entry-meta"> |
| <div class="log-entry-meta-line">(Meta 0)</div> |
| ... |
| <div class="log-entry-meta-line">(Meta N)</div> |
| </div> |
| <div class="log-entry-content"> |
| LINE #0 |
| ... |
| LINE #N |
| </div> |
| </div> |
| ... |
| |
| |
| Note that we can't use templating to show/hide the log dividers, |
| since our positional log insertion requires them to be present and |
| move along with insertions as points of reference. |
| --> |
| |
| <div id="logSplit" class="logFetchButtonContainer"> |
| <!-- Insert point (prepend for head, append for tail). --> |
| <paper-button id="logSplitUp" |
| class="logFetchButton logSplitUpButton giant" |
| text="Load Above" |
| disabled="[[streamAnchorsNotClickable]]" |
| on-click="_handleUpClick"> |
| </paper-button> |
| <paper-button id="logSplitDown" |
| class="logFetchButton logSplitDownButton giant" |
| text="Load Below" |
| disabled="[[streamAnchorsNotClickable]]" |
| on-click="_handleDownClick"> |
| </paper-button> |
| </div> |
| |
| <div id="logBottom" class="logFetchButtonContainer"> |
| <!-- |
| Bottom of the log stream (red bottom line). When tail is complete, |
| all future logs get prepended to this. |
| --> |
| <paper-button id="logBottomButton" |
| class="logFetchButton logBottomButton giant" |
| disabled="[[streamAnchorsNotClickable]]" |
| on-click="_handleBottomClick"> |
| <iron-icon icon="arrow-drop-down"></iron-icon> |
| </paper-button> |
| </div> |
| <div id="logEnd"></div> |
| </div> |
| </div> |
| |
| </div> |
| |
| <template is="dom-if" if="{{statusBar}}"> |
| <div id="status-bar">{{statusBar.value}}</div> |
| </template> |
| </template> |
| |
| </dom-module> |
| |
| <script> |
| "use strict"; |
| |
| Polymer({ |
| is: "logdog-stream-view", |
| |
| properties: { |
| /** The name ([host][:port]) of the pRPC host. */ |
| host: { |
| type: String, |
| notify: true, |
| }, |
| |
| /** |
| * An array of log stream names to load and mux. |
| * |
| * Each stream entry is a valid stream path prefixed with that stream's |
| * project. For example, for stream "foo/bar/+/baz" in project "chromium", |
| * the stream path would be: "chromium/foo/bar/+/baz". |
| */ |
| streams: { |
| type: Array, |
| value: [], |
| notify: true, |
| observer: "_streamsChanged", |
| }, |
| |
| /** |
| * If true, use "mobile" behavior (see model.ts). |
| * |
| * This will try and load in smaller chunks to consume less data by |
| * default. |
| */ |
| mobile: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| /** |
| * The number of logs to load before forcing a page refresh. |
| * |
| * The smaller the value, the smoother the page will behave while logs are |
| * loading. However, the logs will also load slower because of forced |
| * renders in between elements. |
| */ |
| burst: { |
| type: Number, |
| value: 1000, |
| notify: true, |
| }, |
| |
| /** |
| * If true, render metadata blocks alongside their log entries. |
| * |
| * This will cause significantly more HTML elements during rendering (so |
| * that each metadata element can show up next to its row) and greatly |
| * slow the viewer down. |
| */ |
| metadata: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| /** If true, show log metadata column. */ |
| showMetadata: { |
| type: Boolean, |
| value: false, |
| observer: "_showMetadataChanged", |
| }, |
| |
| /** If true, wrap log lines to the screen. */ |
| wrapLines: { |
| type: Boolean, |
| value: false, |
| observer: "_wrapLinesChanged", |
| }, |
| |
| /** |
| * If true, automatically scroll the page to the bottom of the logs |
| * while they are streaming. |
| */ |
| follow: { |
| type: Boolean, |
| value: false, |
| observer: "_followChanged", |
| }, |
| |
| /** |
| * True if the stream is capable of being split. |
| * |
| * This is set by the viewer via "_updateControls". |
| * |
| * A split is a visual break in the log continuity, where content above |
| * the split contains logs starting at the beginning of the stream, while |
| * content below the split contains data from the last log entry in the |
| * stream at the time of the split. |
| */ |
| canSplit: { |
| type: Boolean, |
| value: false, |
| readOnly: true, |
| }, |
| |
| /** |
| * True if log streaming controls should be visible. |
| * |
| * This is set by the viewer via "_updateControls". |
| */ |
| showStreamingControls: { |
| type: Boolean, |
| value: true, |
| readOnly: true, |
| }, |
| |
| /** |
| * True if the view is currently split. |
| * |
| * This is set by the viewer via "_updateControls". |
| */ |
| isSplit: { |
| type: Boolean, |
| value: false, |
| readOnly: true, |
| }, |
| |
| /** |
| * True if the stream anchors (split bar buttons) can be clicked. |
| */ |
| streamAnchorsNotClickable: { |
| type: Boolean, |
| computed: |
| '_computeAnchorsNotClickable(playing, showStreamingControls)', |
| }, |
| |
| /** |
| * True if the viewer should automatically load more logs after the |
| * previous batch has finished. |
| */ |
| playing: { |
| type: Boolean, |
| value: false, |
| observer: "_playingChanged", |
| }, |
| |
| /** |
| * (Computed) the iron-icon name to use for the playing button. |
| */ |
| playingIconName: { |
| type: String, |
| computed: '_computePlayingIconName(playing)', |
| }, |
| |
| /** |
| * True if the automatic loading direction should backfill from the top |
| * (load above the split) instead of continue laoding new logs (load below |
| * the split). |
| */ |
| backfill: { |
| type: Boolean, |
| value: false, |
| observer: "_backfillChanged", |
| }, |
| |
| /** |
| * (Computed) the iron-icon name of the backfill button icon. |
| */ |
| backfillIconName: { |
| type: String, |
| computed: '_computeBackfillIconName(backfill)', |
| }, |
| |
| /** If populated, the stream name at the top will link to this URL. */ |
| streamLinkUrl: { |
| type: String, |
| value: null, |
| notify: true, |
| }, |
| |
| /** |
| * The current stream status. This is an Array of objects: |
| * obj.name is the name of the stream. |
| * obj.desc is the status description of the stream. |
| */ |
| streamStatus: { |
| type: Array, |
| value: null, |
| notify: true, |
| readOnly: true, |
| }, |
| |
| /** |
| * The text content of the status element at the bottom of the page. |
| */ |
| statusBar: { |
| type: String, |
| value: null, |
| readOnly: true, |
| }, |
| }, |
| |
| created: function() { |
| this._view = new LogDog.View(this); |
| }, |
| |
| attached: function() { |
| this._view.reset(); |
| }, |
| |
| detached: function() { |
| this._view.detach(); |
| }, |
| |
| stop: function() { |
| this._view.stop(); |
| }, |
| |
| _polymerAppendChild: function(e) { |
| Polymer.dom(this.root).appendChild(e); |
| }, |
| |
| _handleMouseWheel: function(e) { |
| this._view.handleMouseWheel(); |
| }, |
| |
| _handleDownClick: function(e) { |
| this._view.handleDownClick(); |
| }, |
| |
| _handleUpClick: function(e) { |
| this._view._handleUpClick(); |
| }, |
| |
| _handleBottomClick: function(e) { |
| this._view.handleBottomClick(); |
| }, |
| |
| /** Called when the bound log stream variables has changed. */ |
| _streamsChanged: function() { |
| this._view.handleStreamsChanged(); |
| }, |
| |
| /** |
| * Callback when "showMetadata" has changed. This adds/removes the |
| * "showMeta" CSS class from the metadata column. |
| */ |
| _showMetadataChanged: function(v) { |
| this.toggleClass("showMeta", v, this.$.logs); |
| }, |
| |
| /** |
| * Callback when "wrapLines" has changed. This adds/removes the |
| * "wrapLines" CSS class to the log data. |
| */ |
| _wrapLinesChanged: function(v) { |
| this.toggleClass("wrapLines", v, this.$.logs); |
| }, |
| |
| /** Callback when "follow" has changed. */ |
| _playingChanged: function(v) { |
| this._view.handlePlayingChanged(v); |
| }, |
| |
| _computePlayingIconName: function(playing) { |
| return ( (playing) ? |
| "av:pause-circle-outline" : "av:play-circle-outline" ); |
| }, |
| |
| /** Callback when "follow" has changed. */ |
| _backfillChanged: function(v) { |
| this._view.handleBackfillChanged(v); |
| }, |
| |
| _computeBackfillIconName: function(backfill) { |
| return ( (backfill) ? |
| "editor:border-bottom" : "editor:border-top" ); |
| }, |
| |
| /** Callback when "follow" has changed. */ |
| _followChanged: function(v) { |
| this._view.handleFollowChanged(v); |
| }, |
| |
| /** Callback when "split" button has been clicked. */ |
| _splitClicked: function() { |
| this._view.handleSplitClicked(); |
| }, |
| |
| /** Callback when "split" button has been clicked. */ |
| _scrollToSplit: function() { |
| this._maybeScrollToElement(this.$.logSplit, true, true); |
| }, |
| |
| _updateSplitVisible: function(v) { |
| this.toggleClass("logFetchButtonVisible", v, this.$.logSplit); |
| }, |
| |
| _updateBottomVisible: function(v) { |
| this.toggleClass("logFetchButtonVisible", v, this.$.logBottom); |
| }, |
| |
| _computeAnchorsNotClickable: function(playing, showStreamingControls, |
| rendering) { |
| // Anchors are not clickable if we're playing or the controls are |
| // not visible. |
| return ( playing || (!showStreamingControls) || rendering ); |
| }, |
| |
| /** Filter function to invert a value. */ |
| _not: function(v) { |
| return (!v); |
| }, |
| |
| _onSignin: function() { |
| this._view.handleSignin(); |
| }, |
| }); |
| </script> |