blob: 3b42e672e83cde9ce0969194ba29b2a02f771ab6 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/components/core-icon-button/core-icon-button.html">
<link rel="import" href="/dashboard/static/simple_xhr.html">
<polymer-element name="quick-log"
attributes="logLabel logNamespace logName logFilter
loadOnReady expandOnReady xsrfToken">
<template>
<style>
/**
* These are the intended layouts for quick-log element:
* 1. Height grows with logs and keep a maximum height.
* 2. Width is inherit by parent container unless specified.
* 3. Holds HTML logs and preserves line-break.
*/
#container {
min-width: 800px;
width: 100%;
margin: 0 auto;
}
.label-container {
text-align: right;
padding-bottom: 5px;
padding-right: 2px;
}
.arrow-right::after {
content: '▸';
}
.arrow-down::after {
content: '▾';
}
.toggle-arrow {
height: 100%;
width: 20px;
margin-top: 2px;
display: block;
cursor: pointer;
user-select: none;
}
#log-label {
padding-left: 18px;
background-position: left center;
background-repeat: no-repeat;
vertical-align: middle;
color: #15c;
user-select: none;
}
#content {
display: block;
position: relative;
width: 100%;
}
#inner-content {
display: block;
position: absolute;
width: 100%;
}
.content-bar {
display: block;
background-color: #f5f5f5;
padding: 0 5px 0 10px;
border-bottom: 1px solid #ebebeb;
text-align: right;
}
#wrapper {
overflow: scroll;
max-height: 250px;
display: block;
overflow: auto;
}
#logs {
width: 100%;
height: 100%;
border-bottom: 1px solid #e5e5e5;
border-collapse: collapse;
}
#logs tr {
border-bottom: 1px solid #e5e5e5;
}
#logs tr:hover {
background-color: #ffffd6
}
#logs td {
margin: 0;
padding: 0;
}
#logs tr td:first-child {
vertical-align: top;
text-align: left;
width: 23px;
}
#logs td .message {
position: relative;
height: 26px;
}
#logs td .message.expand {
height: auto !important;
}
#logs td .message pre {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
margin: 0;
padding: 5px 0;
font-family: inherit;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Wraps text and also preserves line break.*/
#logs td .message.expand pre {
white-space: pre-line;
position: static;
height: auto !important;
}
.loading-img {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>
<div id="container">
<div class="label-container">
<core-icon-button id="log-label" icon="expand-more" on-click="{{toggleView}}">
{{logLabel}}
</core-icon-button>
</div>
<div id="content" style="display:none">
<div id="inner-content">
<div class="content-bar">
<core-icon-button id="refresh-btn" icon="refresh" on-click="{{refresh}}">
</core-icon-button>
</div>
<div id="wrapper">
<table id="logs"></table>
<template bind if="{{stepLoading}}">
<img class="loading-img"
height="25"
width="25"
src="//www.google.com/images/loading.gif">
</template>
<template bind if="{{errorMessage}}">
<div class="error">{{errorMessage}}</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
'use strict';
Polymer('quick-log', {
MAX_LOG_REQUEST_SIZE: 100,
/**
* Custom element lifecycle callback, called once this element is ready.
*/
ready: function() {
this.logList = [];
this.xhr = null;
if (this.loadOnReady) {
this.getLogs();
if (this.expandOnReady) {
this.show();
}
}
},
/**
* Initializes log parameters and send a request to get logs.
* @param {string} logLabel The label of log handle for
* expanding log container.
* @param {string} logNamespace Namespace name.
* @param {string} logName Log name.
* @param {string} logFilter A regex string to filter logs.
*/
initialize: function(logLabel, logNamespace, logName, logFilter) {
this.logLabel = logLabel;
this.logNamespace = logNamespace;
this.logName = logName;
this.logFilter = logFilter;
this.clear();
this.getLogs();
},
/**
* Sends XMLHttpRequest to get logs.
* @param {boolean} latest True to get the latest logs,
False to get older logs.
*/
getLogs: function(latest) {
latest = ((latest == undefined) ? true : latest);
if (this.xhr) {
this.xhr.abort();
this.xhr = null;
}
this.setState('loading');
var params = {
namespace: this.logNamespace,
name: this.logName,
size: this.MAX_LOG_REQUEST_SIZE,
xsrf_token: this.xsrfToken
};
if (this.logFilter) {
params['filter'] = this.logFilter;
}
if (this.logList.length > 0) {
if (latest) {
params['after_timestamp'] = this.logList[0].timestamp;
} else {
var lastLog = this.logList[this.logList.length - 1];
params['before_timestamp'] = lastLog.timestamp;
}
}
this.xhr = simple_xhr.send('/get_logs', params,
function(logs) {
this.errorMessage = null;
this.setState('finished');
if (logs.length > 0) {
this.updateLogs(logs);
}
}.bind(this),
function(msg) {
this.errorMessage = msg;
this.setState('finished');
}.bind(this)
);
},
/**
* Updates current displaying logs with new logs.
* @param {Array.<Object>} newLogs Array of log objects.
*/
updateLogs: function(newLogs) {
var insertBefore = true;
if (this.logList.length) {
var lastTimestamp = newLogs[newLogs.length - 1].timestamp;
insertBefore = lastTimestamp >= this.logList[0].timestamp;
}
var table = this.$.logs;
if (insertBefore) {
newLogs.reverse();
}
for (var i = 0; i < newLogs.length; i++) {
this.removeLog(table, newLogs[i]);
this.insertLog(table, newLogs[i], insertBefore);
}
this.updateHeight();
},
/**
* Inserts a log into HTML table.
* @param {Object} table Table HTML element.
* @param {Object} log A log object.
* @param {boolean} insertBefore true to prepend, false to append.
*/
insertLog: function(table, log, insertBefore) {
if (insertBefore) {
this.logList.unshift(log);
} else {
this.logList.push(log);
}
var row = document.createElement('tr');
var expandTd = document.createElement('td');
row.appendChild(expandTd);
var span = document.createElement('span');
span.className = 'toggle-arrow arrow-right';
expandTd.appendChild(span);
var td = document.createElement('td');
var messageDiv = document.createElement('div');
messageDiv.className = 'message';
row.appendChild(td);
td.appendChild(messageDiv);
messageDiv.innerHTML = '<pre>' + log.message + '</pre>';
span.onclick = this.onLogToggleClick.bind(this, messageDiv);
table.insertBefore(row, table.childNodes[0]);
},
/**
* Removes a log.
* @param {Object} table Table HTML element.
* @param {Object} log A log object.
*/
removeLog: function(table, log) {
for (var i = 0; i < this.logList.length; i++) {
if (log.id == this.logList[i].id) {
this.logList.splice(i, 1);
table.deleteRow(i);
}
}
},
/**
* Toggles show/hide log.
*/
onLogToggleClick: function(messageDiv, e) {
var arrowIcon = e.target;
if (arrowIcon.className.indexOf('arrow-right') > -1) {
arrowIcon.className = 'toggle-arrow arrow-down';
messageDiv.className = 'message expand';
} else {
arrowIcon.className = 'toggle-arrow arrow-right';
messageDiv.className = 'message';
}
this.updateHeight();
},
/**
* Specifies loading state.
*/
setState: function(state) {
switch (state) {
case 'loading':
this.stepLoading = true;
this.$['refresh-btn'].disabled = true;
break;
case 'finished':
this.stepLoading = false;
this.$['refresh-btn'].disabled = false;
break;
}
},
/**
* Toggles show/hide log container.
*/
toggleView: function() {
if (this.$.content.style.display == '') {
this.hide();
} else {
this.show();
this.scrollIntoView();
}
},
/**
* Scrolls into view if log container is out of view.
*/
scrollIntoView: function() {
var el = this.$.content;
var bottomOfPage = window.pageYOffset + window.innerHeight;
var bottomOfEl = el.offsetTop + el.offsetHeight;
if (bottomOfEl > bottomOfPage) {
el.scrollIntoView();
}
},
/**
* Refreshes log container.
*/
refresh: function() {
if (this.stepLoading) {
return;
}
this.getLogs();
},
/**
* Shows log container.
*/
show: function() {
this.$['log-label'].icon = 'expand-less';
this.$.content.style.display = '';
if (!this.stepLoading) {
this.$['refresh-btn'].disabled = false;
}
this.updateHeight();
},
/**
* Hides log container.
*/
hide: function() {
this.$['log-label'].icon = 'expand-more';
this.$.content.style.display = 'none';
this.$['refresh-btn'].disabled = true;
},
/**
* Clear logs.
*/
clear: function() {
this.logList = [];
this.$.logs.innerHTML = '';
},
/**
* Since we use absolute inner div, we'll keep the parent div updated
* to make sure this element doesn't overlap with elements below.
*/
updateHeight: function() {
this.$.content.style.height = (
this.$['inner-content'].offsetHeight + 'px');
}
});
</script>
</polymer-element>