blob: 1a23ab344ffbda503449fe7f69c799e6f0ac8410 [file]
<!--
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>