Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(362)

Unified Diff: web/inc/logdog-stream-view/logdog-stream-view.html

Issue 2543323004: Rewrite LogDog log viewer app. (Closed)
Patch Set: Control all fetch sizes, fix follow on initial click, fix small fetch when auth is retrid. Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: web/inc/logdog-stream-view/logdog-stream-view.html
diff --git a/web/inc/logdog-stream-view/logdog-stream-view.html b/web/inc/logdog-stream-view/logdog-stream-view.html
index 563bc0a1adef491f937e6df58803d8a43cbc1c1e..f90fdd7f98abe5597a2d7d7cfbec182031026a39 100644
--- a/web/inc/logdog-stream-view/logdog-stream-view.html
+++ b/web/inc/logdog-stream-view/logdog-stream-view.html
@@ -6,13 +6,12 @@
<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/paper-checkbox/paper-checkbox.html">
-
-<link rel="import" href="../logdog-stream/logdog-stream.html">
-<link rel="import" href="../logdog-stream/logdog-error.html">
-<link rel="import" href="../luci-sleep-promise/luci-sleep-promise.html">
-<link rel="import" href="logdog-stream-fetcher.html">
-<link rel="import" href="logdog-stream-query.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">
+<link rel="import" href="">
<!--
An element for rendering muxed LogDog log streams.
@@ -20,12 +19,32 @@ An element for rendering muxed LogDog log streams.
<dom-module id="logdog-stream-view">
<template>
- <style>
- .buttons {
+ <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;
}
- #stream-status {
+ .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;
@@ -33,7 +52,7 @@ An element for rendering muxed LogDog log streams.
}
#logContent {
- padding-top: 20px;
+ padding-top: 54px; /* Pad around buttons */
background-color: white;
}
@@ -43,53 +62,89 @@ An element for rendering muxed LogDog log streams.
}
.log-entry-meta {
- vertical-align: top;
- padding: 0 8px 0 0;
- margin: 0 0 0 0;
- float: left;
+ 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;
- /* Fixed width, break word if necessary. */
- width: 150px;
- word-break: break-word;
-
- /* Can be toggled by applying .showMeta class to #logs. */
+ /* 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: block;
+ display: flex;
}
- .log-entry-content {
+ /* .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;
- }
- .log-entry-line {
- padding-left: 0;
+ border-bottom: 1px solid #CCCCCC;
/* Can be toggled by applying .wrapLines class to #logs. */
white-space: pre;
}
- .wrapLines .log-entry-line {
+
+ /*.wrapLines .log-entry-content { */
+ .wrapLines .log-entry-chunk {
white-space: pre-wrap;
word-break: break-word;
}
- .log-entry-line:nth-last-child(2) {
- border-bottom: 1px solid #CCCCCC;
+ .logFetchButtonContainer {
+ height: auto;
+ display: none;
+ flex-direction: row;
+ background-color: rgba(0, 0, 0, 0.2);
+ padding: 2px;
}
- #bottom {
+ .logFetchButtonVisible {
+ display: flex !important;
+ }
+
+ .logFetchButton {
+ width: 100%;
+ height: 18px;
+ }
+
+ .logSplitUpButton {
+ background: yellow;
+ }
+ .logSplitDownButton {
+ background: green;
+ }
+
+ .logBottomButton {
background-color: lightcoral;
- height: 2px;
- margin-bottom: 10px;
+ }
+
+ #logEnd {
+ margin-bottom: 30px;
+ background-color: gray;
+ }
+
+ .clickable-log-anchor {
+ height: 24px;
}
#status-bar {
@@ -107,32 +162,74 @@ An element for rendering muxed LogDog log streams.
}
</style>
- <google-signin-aware
- id="aware"
- on-google-signin-aware-success="_onSignin"></google-signin-aware>
-
<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 class="main-view">
- <div class="buttons">
- <paper-checkbox checked="{{showMetadata}}">
- Show Metadata
- </paper-checkbox>
- <paper-checkbox checked="{{wrapLines}}">
- Wrap Lines
- </paper-checkbox>
- <paper-checkbox checked="{{follow}}">
- Follow
- </paper-checkbox>
+ <div id="mainView">
+ <div id="buttons">
+ <!-- If we have exactly one stream, we will enable users to split. -->
+ <template is="dom-if" if="{{showStreamingControls}}">
+ <template is="dom-if" if="{{canSplit}}">
+ <paper-icon-button title="Split, load logs from end"
+ 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>
+
+ <paper-icon-button class="paper-icon-button-highlight" toggles
+ title="Automatically scroll to new logs." icon="icons:update"
+ active="{{follow}}">
+ </paper-icon-button>
+
+ <paper-icon-button class="paper-icon-button-highlight" toggles
+ title="Automatically load logs." icon="{{playingIconName}}"
+ active="{{playing}}">
+ </paper-icon-button>
+
+ <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. -->
- <template is="dom-if" if="{{streamStatus}}">
- <div id="stream-status">
+ <div id="streamStatus">
+ <template is="dom-if" if="{{streamStatus}}">
<table>
<template is="dom-repeat" items="{{streamStatus}}">
<tr>
@@ -141,11 +238,12 @@ An element for rendering muxed LogDog log streams.
</tr>
</template>
</table>
- </div>
- </template>
+ </template>
+ </div>
<!-- Muxed log content. -->
- <div id="logContent" on-mousewheel="_handleMouseWheel">
+ <div id="logContent"
+ on-mousewheel="_handleMouseWheel">
<div id="logs">
<!-- Content will be populated with JavaScript as logs are loaded.
@@ -156,18 +254,49 @@ An element for rendering muxed LogDog log streams.
<div class="log-entry-meta-line">(Meta N)</div>
</div>
<div class="log-entry-content">
- <div class="log-entry-line">LINE #0</div>
+ LINE #0
...
- <div class="log-entry-line">LINE #N</div>
+ 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>
- <!-- Current red bottom line. -->
- <div id="bottom"></div>
+ <div id="logSplit" class="logFetchButtonContainer">
+ <!-- Insert point (prepend for head, append for tail). -->
+ <paper-button id="logSplitUp"
+ class="logFetchButton logSplitUpButton giant"
+ disabled="[[streamAnchorsNotClickable]]"
+ on-click="_handleUpClick">
+ <iron-icon icon="file-upload"></iron-icon>
+ </paper-button>
+ <paper-button id="logSplitDown"
+ class="logFetchButton logSplitDownButton giant"
+ disabled="[[streamAnchorsNotClickable]]"
+ on-click="_handleDownClick">
+ <iron-icon icon="file-download"></iron-icon>
+ </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>
@@ -206,6 +335,16 @@ An element for rendering muxed LogDog log streams.
observer: "_streamsChanged",
},
+ toolbarAnchor: {
+ type: Object,
+ value: null,
+ },
+
+ mobile: {
+ type: Boolean,
+ value: false,
+ },
+
/**
* The number of logs to load before forcing a page refresh.
*
@@ -219,6 +358,18 @@ An element for rendering muxed LogDog log streams.
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,
@@ -243,6 +394,52 @@ An element for rendering muxed LogDog log streams.
observer: "_followChanged",
},
+ canSplit: {
+ type: Boolean,
+ value: false,
+ readOnly: true,
+ },
+
+ showStreamingControls: {
+ type: Boolean,
+ value: true,
+ readOnly: true,
+ },
+
+ isSplit: {
+ type: Boolean,
+ value: false,
+ readOnly: true,
+ },
+
+ streamAnchorsNotClickable: {
+ type: Boolean,
+ computed:
+ '_computeAnchorsNotClickable(playing, showStreamingControls)',
+ },
+
+ playing: {
+ type: Boolean,
+ value: false,
+ observer: "_playingChanged",
+ },
+
+ playingIconName: {
+ type: String,
+ computed: '_computePlayingIconName(playing)',
+ },
+
+ backfill: {
+ type: Boolean,
+ value: false,
+ observer: "_backfillChanged",
+ },
+
+ backfillIconName: {
+ type: String,
+ computed: '_computeBackfillIconName(backfill)',
+ },
+
/**
* The current stream status. This is an Array of objects:
* obj.name is the name of the stream.
@@ -265,304 +462,358 @@ An element for rendering muxed LogDog log streams.
},
},
- ready: function() {
- this._scheduledWrite = null;
- this._buffer = null;
- this._currentLogBuffer = null;
- this._authCallback = null;
+ created: function() {
+ this._scrollTimeoutId = null;
+
+ // Create "onScrollHandler", which just invokes "_onScroll" while bound
+ // to "this". We create it here so we can unregister it later, since
+ // "bind" returns a modified value.
+ this._onScrollHandler = function(e) { this._onScroll(e); }.bind(this);
+ },
+
+ attached: function() {
+ require(["inc/rpc/client", "inc/logdog-stream-view/viewer"],
+ function(client, viewer) {
+ // Instantiate our view, and install callbacks.
+ this._v = viewer;
+ this._model = new viewer.Model({
+ client: new client.luci_rpc.Client(this.$.client),
+ mobile: this.mobile,
+
+ pushLogEntries: this._pushLogEntries.bind(this),
+ clearLogEntries: this._clearLogEntries.bind(this),
+ updateControls: this._updateControls.bind(this),
+ locationIsVisible: this._locationIsVisible.bind(this),
+ });
+ this._streamsChanged();
+ }.bind(this));
+
+ window.addEventListener('scroll', this._onScrollHandler);
},
detached: function() {
- this.stop();
+ // Unregsiter event handlers.
+ window.removeEventListener('scroll', this._onScrollHandler);
+
+ // Reset state.
+ this.reset();
},
stop: function() {
- this._cancelFetch(true);
+ this.reset();
},
/** Clears state and begins fetching log data. */
reset: function() {
- this._resetLogState();
+ if ( this._model ) {
+ this._model.reset();
+ }
+ this._resetScroll();
+ this._model = null;
+ this._renderedLogs = false;
+ },
- this._resolveStreams().then(function(streams) {
- this._resetToStreams(streams);
- }.bind(this)).catch(function(error) {
- this._loadStatusBar("Failed to resolve streams:" + error);
- throw error;
- }.bind(this));
+ /**
+ * Called each time a scroll event is fired. Since this can be really
+ * frequent, this will kick off a "scroll handler" in the background at an
+ * interval. Multiple scroll events within that interval will only result
+ * in one scroll handler invocation.
+ */
+ _onScroll: function(e) {
+ if ( this._scrollTimeoutId ) {
+ return;
+ }
+
+ window.setTimeout(function() {
+ this._handleScrollEvent(e);
+ }.bind(this), 100);
},
- /** Clears all current logs. */
- _resetLogState: function() {
- this._cancelFetch(true);
+ /** Actual scroll event handler. */
+ _handleScrollEvent: function(e) {
+ this._resetScroll();
- // Remove all current log elements. */
- while (this.$.logs.hasChildNodes()) {
- this.$.logs.removeChild(this.$.logs.lastChild);
+ // Update our button bar position to be relative to the parent's height.
+ this._adjustToTop(this.$.buttons);
+ this._adjustToTop(this.$.streamStatus);
+ },
+
+ _adjustToTop: function(elem) {
+ // Update our button bar position to be relative to the parent's height.
+ var pageRect = this.$.mainView.getBoundingClientRect();
+ var elemRect = elem.getBoundingClientRect();
+ var adjusted = (elem.offsetTop + pageRect.top - elemRect.top);
+ if ( adjusted < 0 ) {
+ adjusted = 0;
}
+ elem.style.top = adjusted;
+ },
- // Clear our buffer and streamer state.
- this._buffer = null;
- this._currentLogBuffer = null;
- if (this._streamer) {
- this._streamer.shutdown();
+ /** Clears asynchornous scroll event status. */
+ _resetScroll: function() {
+ if ( this._scrollTimeoutId !== null ) {
+ window.clearTimeout(this._scrollTimeoutId);
+ this._scrollTimeoutId = null;
}
- this._streamer = null;
},
- _resolveStreams: function() {
- // Separate our configured streams into full stream paths and queries.
- var parts = {
- queries: [],
- streams: [],
- };
- var query = new LogDogQuery(this.project);
- this.streams.map(LogDogStream.splitProject).forEach(function(v) {
- if (LogDogQuery.isQuery(v.path)) {
- parts.queries.push(v);
- } else {
- parts.streams.push(v);
- }
- });
-
- // Resolve any outstanding queries into full stream paths.
- //
- // If we get an authentication error, register to have our query
- // resolution callback invoked on signin changes until it works (or
- // indefinitely).
- var queries = parts.queries.map(function(v) {
- var params = new LogDogQueryParams(v.project).
- path(v.path).
- streamType("text");
- return new LogDogQuery(this.$.client, params);
- }.bind(this));
+ _handleMouseWheel: function(e) {
+ this.follow = false;
+ },
- var issueQuery = function() {
- this._loadStatusBar("Resolving log streams from query...");
- this._authCallback = null;
+ _handleDownClick: function(e) {
+ this._model.fetchLocation(this._v.Location.HEAD, true);
+ },
- return Promise.all(queries.map(function(q) {
- return q.getAll();
- }.bind(this))).then(function(results) {
- this._loadStatusBar(null);
+ _handleUpClick: function(e) {
+ this._model.fetchLocation(this._v.Location.TAIL, true);
+ },
- // Add query results (if any) to streams.
- results.forEach(function(streams) {
- (streams || []).forEach(function(stream) {
- parts.streams.push(stream.stream);
- });
- });
- parts.streams.sort(LogDogStream.cmp);
-
- // Remove any duplicates.
- var seenStreams = {};
- var result = [];
- parts.streams.forEach(function(s) {
- var fullName = s.fullName();
- if (!seenStreams[fullName]) {
- seenStreams[fullName] = s;
- result.push(s);
- }
- });
- return result;
- }.bind(this)).catch(function(error) {
- if (error instanceof LogDogError && error.isUnauthenticated()) {
- // Retry on auth event.
- this._loadStatusBar("Not authorized to execute query. Log in " +
- "with an authorized account.");
- return new Promise(function(resolve) {
- this._authCallback = resolve;
- }.bind(this)).then(issueQuery);
- }
-
- throw error;
- }.bind(this));
- }.bind(this);
- return issueQuery();
+ _handleBottomClick: function(e) {
+ this._model.fetchLocation(this._v.Location.BOTTOM, true);
},
- _resetToStreams: function(streams) {
- // Unique streams.
- if (!streams.length) {
- this._loadStatusBar("No log streams.");
+ /** Called when the bound log stream variables has changed. */
+ _streamsChanged: function() {
+ if( ! this._model ) {
return;
}
- console.log("Loading log streams:", streams);
- this._loadStatusBar("Loading stream data...");
- streams.sort(LogDogStream.cmp);
-
- // Create a _BufferedStream for each stream.
- var bufStreams = streams.map(function(stream, idx) {
- return new _BufferedStream(stream, this.$.client,
- (streams.length > 1), function(bs) {
- this._updateStreamStatus(bs, idx);
- }.bind(this));
- }.bind(this));
- this._buffer = new _LogStreamBuffer();
- this._buffer.setStreams(bufStreams)
+ this._model.resolve(this.streams).then( function() {
+ // If we're not on mobile, start with playing state.
+ this.playing = (!this.mobile);
- this._streamer = new _LogStreamer(this._buffer, this.burst, function(v) {
- this._loadStatusBar(v);
- }.bind(this));
-
- // Construct our initial status content.
- this._setStreamStatus(bufStreams.map(function(bs, idx) {
- return {
- name: (" [.../+/" + bs.stream.name() + "]"),
- desc: bs.description(),
- };
- }.bind(this)));
-
- // Kick off our log fetching.
- this._scheduleWriteNextLogs();
+ // Perform the initial fetch after resolution.
+ this._model.setAutomatic( this.playing );
+ this._model.setTailing( ! this.backfill );
+ this._model.fetch(false);
+ }.bind(this) );
},
- /** Cancels any currently-executing log stream fetch. */
- _cancelFetch: function(clear) {
- this._cancelScheduledWrite();
- this._authCallback = null;
+ _appendMetaLine: function(root, key, value) {
+ var line = document.createElement("div");
+ line.className = "log-entry-meta-line";
- if (clear) {
- this._setStreamStatus(null);
- this._loadStatusBar(null);
+ if ( key != null ) {
+ var e = document.createElement("strong");
+ e.textContent = key;
+ line.appendChild(e);
}
- },
- /** Cancels any scheduled asynchronous write. */
- _cancelScheduledWrite: function() {
- if (this._scheduledWrite) {
- this.cancelAsync(this._scheduledWrite);
- this._scheduledWrite = null;
+ if ( value != null ) {
+ var e = document.createElement("span");
+ e.textContent = value;
+ line.appendChild(e);
}
- },
- /** Called when the bound log stream variables has changed. */
- _streamsChanged: function(v, old) {
- this.reset();
+ root.appendChild(line);
},
- /** Schedules the next asynchronous log write. */
- _scheduleWriteNextLogs: function() {
- // This is called after refresh, so use this opportunity to maybe scroll
- // to the bottom.
- this._maybeScrollToBottom();
+ _pushLogEntries: function(entries, insertion) {
+ // Mark that we've rendered logs (show bars now).
+ this._renderedLogs = true;
- if (! this._scheduledWrite) {
- this._scheduledWrite = this.async(function() {
- this._writeNextLogs()
- }.bind(this));
- }
- },
+ // Build our log entry chunk.
+ var logEntryChunk = document.createElement("div");
+ logEntryChunk.className = "log-entry-chunk";
- /**
- * This is an iterative function that grabs the next set of logs and renders
- * them. Afterwards, it will continue rescheduling itself until there are
- * no more logs to render.
- */
- _writeNextLogs: function() {
- this._cancelScheduledWrite();
-
- this._streamer.load().then(function(entries) {
- // If there are no entries, then we're done.
- if (! entries) {
- // Cancel all fetching state. If our streamer is finished, also clear
- // messages and status.
- if (this._streamer.finished) {
- if (this._streamer.someStreamsFailed) {
- this._cancelFetch(false);
- this._loadStatusBar("Some streams failed to load.");
- } else {
- this._cancelFetch(true);
- }
- } else {
- // No more logs, but also we are not finished. Retry after auth.
- this._authCallback = this._scheduleWriteNextLogs.bind(this);
- }
+ var lastLogEntry = logEntryChunk;
+ var lines = new Array();
+
+ entries.forEach(function(le) {
+ var text = le.text;
+ if (!(text && text.lines)) {
return;
}
- var logEntryChunk = document.createElement("div");
- entries.forEach(function(le) {
- this._appendLogEntry(logEntryChunk, le);
- }.bind(this));
-
- // To have styles apply correctly, we need to add it twice, see
- // https://github.com/Polymer/polymer/issues/3100.
- Polymer.dom(this.root).appendChild(logEntryChunk);
- this.$.logs.appendChild(logEntryChunk);
-
- // Yield so that our browser can refresh. We can't directly use
- // this.async since a timeout of "0" causes immediate execution instead
- // of yielding.
- setTimeout(function() {
- this._scheduleWriteNextLogs();
- }.bind(this), 0);
+ // If we're rendering metadata, render an element per log entry.
+ if( this.metadata ) {
+ var entryRow = document.createElement("div");
+ entryRow.className = "log-entry";
+
+ // Metadata column.
+ var metadataBlock = document.createElement("div");
+ metadataBlock.className = "log-entry-meta";
+
+ this._appendMetaLine(metadataBlock, "Timestamp:", le.timestamp);
+ this._appendMetaLine(metadataBlock, "Stream:", le.desc.name);
+ this._appendMetaLine(metadataBlock, "Index:", le.streamIndex);
+
+ // Log column.
+ var logDataBlock = document.createElement("div");
+ logDataBlock.className = "log-entry-content";
+
+ le.text.lines.forEach(function(line) {
+ lines.push(line.value);
+ });
+
+ logDataBlock.textContent = lines.join("\n");
+ lines.length = 0;
+
+ entryRow.appendChild(metadataBlock);
+ entryRow.appendChild(logDataBlock);
+
+ logEntryChunk.appendChild(entryRow);
+ lastLogEntry = entryRow;
+ } else {
+ // Add this to the lines. We'll assign this directly to logEntryChunk
+ // after the loop.
+ le.text.lines.forEach(function(line) {
+ lines.push(line.value);
+ });
+ }
}.bind(this));
- },
- _appendLogEntry: function(root, le) {
- var text = le.text;
- if (!(text && text.lines)) {
- return 0;
+ if ( ! this.metadata ) {
+ // Only one HTML element: the chunk.
+ logEntryChunk.textContent = lines.join("\n");
+ lastLogEntry = logEntryChunk;
+ }
+
+ // To have styles apply correctly, we need to add it twice, see
+ // https://github.com/Polymer/polymer/issues/3100.
+ Polymer.dom(this.root).appendChild(logEntryChunk);
+
+ // Add the log entry to the appropriate place.
+ var anchor, scrollToTop = false;
+ var forceScroll = false;
+ switch ( insertion ) {
+ case this._v.Location.HEAD:
+ // InsertionPoint.HEAD: PREPEND to "logSplit".
+ this.$.logs.insertBefore(logEntryChunk, this.$.logSplit);
+
+ // If we're not split, scroll to the log bottom. Otherwise, scroll to
+ // the split.
+ anchor = lastLogEntry;
+ break;
+
+ case this._v.Location.TAIL:
+ // InsertionPoint.TAIL: APPEND to "logSplit".
+ anchor = this.$.logSplit;
+
+ // Identify the element *after* our insertion point and scroll to it.
+ // This provides a semblance of stability as we top-insert.
+ //
+ // As a special case, if the next element is the log bottom, just
+ // scroll to the split, since there is no content to stabilize.
+ if ( anchor.nextElementSibling !== this.$.logBottom ) {
+ anchor = anchor.nextElementSibling;
+ }
+
+ // Insert logs by adding them before the sibling following the log
+ // split (append to this.$.logSplit).
+ this.$.logs.insertBefore(logEntryChunk, this.$.logSplit.nextSibling);
+
+ // When tailing, always scroll to the anchor point.
+ scrollToTop = true;
+ forceScroll = true;
+ break;
+
+ case this._v.Location.BOTTOM:
+ // InsertionPoint.BOTTOM: PREPEND to "logBottom".
+ anchor = this.$.logBottom;
+ this.$.logs.insertBefore(logEntryChunk, anchor);
+ break;
}
- // Create elements manually to avoid Polymer overhead for large logs.
- var entryRow = document.createElement("div");
- entryRow.className = "log-entry";
-
- // Metadata column.
- var metadataBlock = document.createElement("div");
- metadataBlock.className = "log-entry-meta";
- entryRow.appendChild(metadataBlock);
-
- var timestampDiv = document.createElement("div");
- timestampDiv.className = "log-entry-meta-line";
- timestampDiv.textContent = le.timestamp;
- metadataBlock.appendChild(timestampDiv);
-
- var nameDiv = document.createElement("div");
- nameDiv.className = "log-entry-meta-line";
- nameDiv.textContent = le.desc.name;
- metadataBlock.appendChild(nameDiv);
-
- var streamDiv = document.createElement("div");
- streamDiv.className = "log-entry-meta-line";
- streamDiv.textContent = le.streamIndex;
- metadataBlock.appendChild(streamDiv);
-
- // Log column.
- var logDataBlock = document.createElement("div");
- logDataBlock.className = "log-entry-content";
- if (le.text) {
- for (var i = 0; i < le.text.lines.length; i++) {
- var lineDiv = document.createElement("div");
- lineDiv.className = "log-entry-line";
- lineDiv.textContent = le.text.lines[i].value;
- logDataBlock.appendChild(lineDiv);
+ this._maybeScrollToElement(anchor, scrollToTop, forceScroll);
+ },
+
+ _clearLogEntries: function() {
+ // Remove all current log elements. */
+ for ( var cur = this.$.logs.firstChild; cur; ) {
+ var del = cur;
+ cur = cur.nextElementSibling;
+ if ( del.classList && del.classList.contains('log-entry-chunk') ) {
+ this.$.logs.removeChild(del);
}
}
- entryRow.appendChild(logDataBlock);
- root.appendChild(entryRow);
+ },
+
+ _locationIsVisible: function(l) {
+ var anchor;
+ switch( l ) {
+ case this._v.Location.HEAD:
+ case this._v.Location.TAIL:
+ anchor = this.$.logSplit;
- return le.text.lines.length;
+ case this._v.Location.BOTTOM:
+ anchor = this.$.logBottom;
+
+ default:
+ return false;
+ }
+ return this._elementInViewport(anchor);
},
- _updateStreamStatus: function(bs, idx) {
- var origStatus = this.streamStatus[idx];
- this.splice("streamStatus", idx, 1, {
- name: origStatus.name,
- desc: bs.description(),
- });
+ _updateControls: function(c) {
+ this._setCanSplit(c.canSplit);
+ this._setIsSplit(c.split);
+ this.toggleClass("logFetchButtonVisible",
+ (c.split && this._renderedLogs), this.$.logSplit);
+ this.toggleClass("logFetchButtonVisible",
+ (c.bottom && this._renderedLogs), this.$.logBottom);
+
+ this._setShowStreamingControls( !c.fullyLoaded );
+ if ( c.fullyLoaded ) {
+ this.playing = false;
+ }
+
+ switch( c.loadingState ) {
+ case this._v.LoadingState.NONE:
+ this._loadStatusBar(null);
+ break;
+ case this._v.LoadingState.RESOLVING:
+ this._loadStatusBar("Resolving stream names...");
+ break;
+ case this._v.LoadingState.LOADING:
+ this._loadStatusBar("Loading streams...");
+ break;
+ case this._v.LoadingState.RENDERING:
+ this._loadStatusBar("Rendering logs.");
+ break;
+ case this._v.LoadingState.NEEDS_AUTH:
+ this._loadStatusBar("Not authenticated. Please log in.");
+ break;
+ case this._v.LoadingState.ERROR:
+ this._loadStatusBar("Error loading streams (see console).");
+ break;
+ }
+
+ this._setStreamStatus(c.streamStatus);
},
- /** Scrolls to the bottom if "follow" is enabled. */
- _maybeScrollToBottom: function() {
- if (this.follow) {
- this.$.bottom.scrollIntoView({
- "behavior": "smooth",
- "block": "end",
- });
+ /** Scrolls to the follow anchor point. */
+ _maybeScrollToFollow: function() {
+ // Determine our anchor element.
+ var e;
+ if ( this.isSplit && this.backfill ) {
+ // Centering on the split element, at the bottom of the page.
+ e = this.$.logSplit;
+ } else {
+ // Scroll to the bottom of the page.
+ e = this.$.logEnd;
+ }
+
+ this._maybeScrollToElement(e, false, false);
+ },
+
+ /**
+ * Scrolls to the specified element, centering it at the top or bottom of
+ * the view. By default,t his will only happen if "follow" is enabled;
+ * however, it can be forced via "force".
+ */
+ _maybeScrollToElement: function(element, topOfView, force) {
+ if (this.follow || force) {
+ if ( topOfView ) {
+ element.scrollIntoView({
+ behavior: "auto",
+ block: "end",
+ });
+ } else {
+ // Bug? "block: start" doesn't seem to work the same as false.
+ element.scrollIntoView(false);
+ }
}
},
@@ -572,7 +823,8 @@ An element for rendering muxed LogDog log streams.
*/
_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.
@@ -580,14 +832,77 @@ An element for rendering muxed LogDog log streams.
_wrapLinesChanged: function(v) {
this.toggleClass("wrapLines", v, this.$.logs);
},
+
+ /** Callback when "follow" has changed. */
+ _playingChanged: function(v) {
+ if( ! this._model ) {
+ return;
+ }
+
+ // If we're playing, begin log fetching.
+ this._model.setAutomatic(v);
+ },
+
+ _computePlayingIconName: function(playing) {
+ return ( (playing) ?
Ryan Tseng 2016/12/06 03:38:11 is this backwards?
+ "av:pause-circle-outline" : "av:play-circle-outline" );
+ },
+
+ /** Callback when "follow" has changed. */
+ _backfillChanged: function(v) {
+ if( ! this._model ) {
+ return;
+ }
+
+ // If we're backfilling, then we're not tailing.
+ this._model.setTailing(!v);
+ },
+
+ _computeBackfillIconName: function(backfill) {
+ return ( (backfill) ?
+ "editor:border-bottom" : "editor:border-top" );
+ },
+
/** Callback when "follow" has changed. */
_followChanged: function(v) {
- this._maybeScrollToBottom();
- },
+ if ( ! this._model ) {
+ return;
+ }
- /** Callback for when the mouse wheel has scrolled. Disables follow. */
- _handleMouseWheel: function() {
- this.follow = false;
+ if ( v ) {
+ // If follow is toggled on, automatically begin playing.
+ this.playing = true;
+ this._maybeScrollToFollow();
+ }
+ },
+
+ /** Callback when "split" button has been clicked. */
+ _splitClicked: function() {
+ if( ! this._model ) {
+ return;
+ }
+
+ // After a split, toggle off playing.
+ this._model.split();
+ this._model.setTailing(true);
+ this.playing = false;
+ },
+
+ /** Callback when "split" button has been clicked. */
+ _scrollToSplit: function() {
+ this._maybeScrollToElement(this.$.logSplit, true, true);
+ },
+
+ _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);
},
/**
@@ -607,470 +922,30 @@ An element for rendering muxed LogDog log streams.
},
_onSignin: function() {
- var fn = this._authCallback;
- if (fn) {
- this._authCallback = null;
- fn();
+ if ( this._model ) {
+ this._model.notifyAuthenticationChanged();
}
},
- });
- /**
- * Continuously loads log streams from a _LogStreamBuffer and exposes them via
- * callback.
- */
- function _LogStreamer(buffer, burst, statusCallback) {
- this._buffer = buffer;
- this._burst = (burst || 0);
- this._missingDelay = 5000;
- this._statusCallback = statusCallback;
-
- this.finished = false;
- this.someStreamsFailed = false;
-
- this._currentLogBuffer = null;
- }
-
- _LogStreamer.prototype.shutdown = function() {
- this.finshed = true;
- };
-
- _LogStreamer.prototype._setStatus = function(v) {
- if (this._statusCallback) {
- this._statusCallback(v);
- }
- };
-
- _LogStreamer.prototype.load = function() {
- if (this.finished) {
- this._setStatus(null);
- return Promise.resolve(null);
- }
-
- // If we have buffered logs, return them.
- var current = this._currentLogBuffer;
- if (current) {
- // We will track how many log entries that we've rendered. If we exceed
- // this amount, we will force a refresh so the logs appear streaming and
- // the app remains responsive.
- var rendered = 0;
-
- var entries = [];
- for (var le = current.next(); (le); le = current.next()) {
- entries.push(le);
- if (le.text && le.text.lines) {
- rendered += le.text.lines.length;
- }
+ _elementInViewport: function(el) {
+ var top = el.offsetTop;
+ var left = el.offsetLeft;
+ var width = el.offsetWidth;
+ var height = el.offsetHeight;
- if (this._burst > 0 && rendered >= this._burst) {
- break;
- }
+ while(el.offsetParent) {
+ el = el.offsetParent;
+ top += el.offsetTop;
+ left += el.offsetLeft;
}
- // Have we exhausted this buffer?
- if (! current.peek()) {
- this._currentLogBuffer = null;
- }
-
- // Return the bundle of entries.
- return Promise.resolve(entries);
- }
-
- // We didn't have any buffered logs, so either all of our streams are
- // finished or our buffer is empty and needs to be refreshed.
- this._setStatus("Loading log stream data...");
- return this._buffer.nextBuffer().then(function(buf) {
- this.someStreamsFailed = (!!this._buffer._failures.length);
-
- // Check result.
- if (buf === null) {
- if (this._buffer.finished) {
- // No more buffers, we are done.
- console.log("All streams have been exhausted.");
- this.finished = true;
- this._setStatus(null);
- return null;
- }
-
- // The buffer was incomplete. Should we retry after a delay, or do
- // we need to wait for an explicit edge (e.g., auth)?
- if (this._buffer.autoRetry) {
- // Sleep for 5 seconds and try again (waiting).
- console.log("Log stream delayed; sleeping", this._missingDelay,
- "and retry.");
- this._setStatus("Missing log streams, retrying after delay...");
- return new LuciSleepPromise(this._missingDelay).then(function() {
- if (this.finished) {
- console.log("Streamer is deactivated, discarding.");
- return null;
- }
-
- return this.load();
- }.bind(this));
- }
-
- this._setStatus("Some log streams could not be loaded.");
- return null;
- }
-
- // Install the new buffer and re-enter.
- this._currentLogBuffer = buf;
- return this.load();
- }.bind(this)).catch(function(error) {
- this._setStatus("[" + error.name + "] fetching streams: " +
- error.message);
- throw error;
- }.bind(this));
- };
-
- /**
- * Manages an aggregate log stream buffer, consisting of logs punted from a
- * set of zero or more _BufferedStream instances.
- */
- function _LogStreamBuffer() {
- this._streams = null;
- this._active = null;
- this._nextBufferPromise = null;
- this._failures = [];
-
- this.finished = false;
- this._resetIterativeState();
- }
-
- _LogStreamBuffer.prototype.setStreams = function(streams) {
- // TODO(dnj): Make this do a delta with previous streams so we don't lose
- // their already-loaded logs if the page changes.
- this._streams = streams.map(function(bs, i) {
- return {
- bs: bs,
- active: true,
- buffer: new _BufferedLogs(),
- };
- });
- this._active = this._streams;
- this._nextBufferPromise = null;
- };
-
- _LogStreamBuffer.prototype._resetIterativeState = function() {
- this.autoRetry = false;
- };
-
- /**
- * Returns a Promise that resolves into a _BufferedLogs instance containing
- * the next set of logs, in order, from the source log streams.
- *
- * The _BufferedLogs bundle may have status flags set, and should be checked.
- *
- * The Promise will also resolve to "null" if there are no more logs in the
- * source streams.
- *
- * If there are errors fetching logs, the Promise will be rejected, and an
- * error will be returned.
- */
- _LogStreamBuffer.prototype.nextBuffer = function() {
- // If we're already are fetching the next buffer, return that Promise.
- if (this._nextBufferPromise) {
- return this._nextBufferPromise;
- }
-
- // Filter our any finished streams from our active list. A stream is
- // finished if it is finished streaming and we don't have a retained buffer
- // from it.
- this._active = this._active.filter(function(entry) {
- return (entry.buffer.peek() || (! (entry.bs.finished || entry.bs.error)));
- })
-
- if (! this._active.length) {
- this.finished = true;
- }
- if (this.finished) {
- // No active streams, so we're finished. Permanently set our promise to
- // the finished state.
- this._nextBufferPromise = Promise.resolve(null);
- return this._nextBufferPromise;
- }
-
- // Fill all buffers for all active streams. This may result in an RPC to
- // load new buffer content for streams whose buffers are empty.
- //
- // RPC failures are handled here:
- // - If the stream reports "not found", we will terminate early and set
- // out status to "waiting". Our owner should retry after a delay.
- // - Otherwise, we will set our status to "error". Our owner should report
- // that an error has occurred while loading stream data.
- this._resetIterativeState();
-
- var incomplete = false;
- this._nextBufferPromise = Promise.all(this._active.map(function(entry) {
- // If the entry's buffer still has data, use it immediately.
- if (entry.buffer.peek()) {
- return entry.buffer;
- }
-
- // Get the next log buffer for each stream. This may result in an RPC.
- return entry.bs.nextBuffer().then(function(buffer) {
- // Retain this buffer, if valid. The stream may have returned a null
- // buffer if it failed to fetch for a legitimate reason. In this case,
- // we will not retain it (since we want entry.buffer to be valid), but
- // will forward the "null" to our aggregate function.
- if (buffer) {
- entry.buffer = buffer;
- } else {
- incomplete = true;
-
- // If this stream is waiting, but not on auth, mark that we should
- // automatically retry.
- if (entry.bs.waiting && !entry.bs.auth) {
- this.autoRetry = true;
- }
- }
- return buffer;
- }.bind(this)).catch(function(error) {
- // Log stream source of error. Raise a generic "failed to buffer"
- // error. This will become a permanent failure.
- console.error("Error loading buffer for", entry.bs.stream.fullName(),
- "(", entry.bs, "): ", error);
- this._failures.push(entry.bs);
- return null;
- }.bind(this));
- }.bind(this))).then(function(buffers) {
- this._nextBufferPromise = null;
-
- // Check each buffer. If any are null, that stream failed to deliver.
- if (incomplete) {
- // We succeeded, but are incomplete. At least one stream failed to
- // deliver and should have state flags set accordingly.
- return null;
- }
-
- // Remove any null buffers. These would be placed here when a stream fails
- // to load. Aggregate as much data from each of our streams as possible.
- buffers = buffers.filter(v => (!!v));
- return this._aggregateBuffers(buffers);
- }.bind(this));
- return this._nextBufferPromise;
- };
-
- _LogStreamBuffer.prototype._aggregateBuffers = function(buffers) {
- switch (buffers.length) {
- case 0:
- // No buffers, so no logs.
- return new _BufferedLogs(null);
- case 1:
- // As a special case, if we only have one buffer, and we assume that its
- // entries are sorted, then that buffer is a return value.
- return new _BufferedLogs(buffers[0].getAll());
- }
-
- // Preload our peek array.
- var incomplete = false;
- var peek = buffers.map(function(buf) {
- var le = buf.peek();
- if (! le) {
- incomplete = true;
- }
- return le;
- });
- if (incomplete) {
- // One of our input buffers had no log entries.
- return new _BufferedLogs(null);
- }
-
- // Assemble our aggregate buffer array.
- // TODO: A binary heap would be pretty great for this.
- var entries = [];
- while (true) {
- // Choose the next stream.
- var earliest = 0;
- for (var i = 1; i < buffers.length; i++) {
- if (_LogStreamBuffer.compareLogs(peek[i], peek[earliest]) < 0) {
- earliest = i;
- }
- }
-
- // Get the next log from the earliest stream.
- entries.push(buffers[earliest].next());
-
- // Repopulate that buffer's "peek" value. If the buffer has no more
- // entries, then we're done.
- var next = buffers[earliest].peek();
- if (!next) {
- return new _BufferedLogs(entries);
- }
- peek[earliest] = next;
- }
- };
-
- _LogStreamBuffer.compareLogs = function(a, b) {
- // If they are part of the same stream, compare prefix indexes.
- if (a.source.stream.samePrefixAs(b.source.stream)) {
- return (a.prefixIndex - b.prefixIndex);
- }
-
- // Compare based on timestamp.
- return a.timestamp - b.timestamp;
- };
-
-
- /**
- * A buffer of ordered log entries from all streams.
- *
- * Assumes total ownership of the input log buffer, which can be null to
- * indicate no logs.
- */
- function _BufferedLogs(logs) {
- this._logs = logs;
- this._index = 0;
- }
-
- _BufferedLogs.prototype.getAll = function() {
- // Pop all logs.
- var logs = this._logs;
- this._logs = null;
- return logs;
- };
-
- _BufferedLogs.prototype.peek = function() {
- return (this._logs) ? (this._logs[this._index]) : (null);
- };
-
- _BufferedLogs.prototype.next = function() {
- if (! (this._logs && this._logs.length)) {
- return null;
- }
-
- // Get the next log and increment our index.
- var log = this._logs[this._index++];
- if (this._index >= this._logs.length) {
- this._logs = null;
- }
- return log;
- };
-
-
- /**
- * Stateful log fetching manager for a single log stream.
- */
- function _BufferedStream(stream, client, oneOfMany, statusCallback) {
- this.stream = stream;
-
- this.error = null;
- this.finished = false;
-
- this._fetcher = new LogDogFetcher(client, stream);
- this._oneOfMany = oneOfMany;
- this._statusCallback = statusCallback;
- this._lastFetchIndex = null;
- }
-
- _BufferedStream.INITIAL_FETCH_SIZE = 4096;
-
- _BufferedStream.prototype._resetIterativeState = function() {
- this.waiting = false;
- this.auth = false;
- this._fireStatusUpdated();
-
- this._currentFetch = null;
- };
-
- _BufferedStream.prototype.nextBuffer = function() {
- if (this._currentFetch) {
- return this._currentFetch;
- }
-
- // Reset per-round state and begin next round fetch.
- this._resetIterativeState();
-
- // If this is the first fetch, and we're not the only log stream being
- // rendered, fetch a small amount so we can (probably) start rendering
- // without waiting for a lot of huge chunks.
- this._fetcher.byteCount = (
- (this._lastFetchIndex === null) && this._oneOfMany) ?
- (_BufferedStream.INITIAL_FETCH_SIZE) : (null);
-
- this._currentFetch = this._fetcher.next().then(function(result) {
- this._currentFetch = null;
-
- // Update our stream information.
- this.finished = this._fetcher.finished;
-
- // Augment each returned log entry with self-descriptive metadata.
- var logs = result.entries;
- if (logs && logs.length) {
- logs.forEach(function(le) {
- le.desc = result.desc;
- le.state = result.state;
- le.source = this;
- }.bind(this));
-
- // Record the latest fetch index.
- this._lastFetchIndex = logs[logs.length - 1].streamIndex;
- }
-
- this._fireStatusUpdated();
- return new _BufferedLogs(logs);
- }.bind(this)).catch(function(error) {;
- // If this is a "not found" error, we assume that the stream is valid, but
- // hasn't been ingested into LogDog yet. Return "null".
- if (error instanceof LogDogError) {
- if (error.isUnauthenticated()) {
- this.waiting = true;
- this.auth = true;
- } else if (error.isNotFound()) {
- this.waiting = true;
- }
-
- // If this is an error that we understand, recover from it, return
- // null, and set our status flags.
- if (this.waiting) {
- // Recover from this error.
- this._currentFetch = null;
- this._fireStatusUpdated();
- return null;
- }
- }
+ return (
+ top < (window.pageYOffset + window.innerHeight) &&
+ left < (window.pageXOffset + window.innerWidth) &&
+ (top + height) > window.pageYOffset &&
+ (left + width) > window.pageXOffset
+ );
+ },
- // Retain this error forever.
- this.error = error;
- throw error;
- }.bind(this));
- return this._currentFetch;
- };
-
- _BufferedStream.prototype._fireStatusUpdated = function() {
- if (this._statusCallback) {
- this._statusCallback(this);
- }
- };
-
- _BufferedStream.prototype.description = function() {
- if (this._waiting) {
- return "(Waiting)";
- }
-
- var pieces = []
- var tidx = this._fetcher.terminalIndex();
- if (this._lastFetchIndex) {
- if (tidx >= 0) {
- pieces.push(this._lastFetchIndex + " / " + tidx);
- } else {
- pieces.push(this._lastFetchIndex + "...");
- }
- }
-
- if (this.error) {
- pieces.push("(Error)");
- } else if (this.auth) {
- pieces.push("(Auth Error)");
- } else if (this.waiting) {
- pieces.push("(Waiting)");
- } else if (!this._fetcher.state) {
- pieces.push("(Fetching)");
- } else if (this._fetcher.finished) {
- pieces.push("(Finished)");
- } else {
- pieces.push("(Streaming)");
- }
- return pieces.join(" ");
- };
+ });
</script>

Powered by Google App Engine
This is Rietveld 408576698