1 <!DOCTYPE html>
2 <!--
3 Copyright 2016 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
6 -->
7
8 <link rel="import" href="/components/core-icon-button/core-icon-button.html">
9
10 <link rel="import" href="/dashboard/static/simple_xhr.html">
11
12 <polymer-element name="quick-log"
13 attributes="logLabel logNamespace logName logFilter
14 loadOnReady expandOnReady xsrfToken">
15 <template>
16 <style>
17 /**
18 * These are the intended layouts for quick-log element:
19 * 1. Height grows with logs and keep a maximum height.
20 * 2. Width is inherit by parent container unless specified.
21 * 3. Holds HTML logs and preserves line-break.
22 */
23 #container {
24 min-width: 800px;
25 width: 100%;
26 margin: 0 auto;
27 }
28
29 .label-container {
30 text-align: right;
31 padding-bottom: 5px;
32 padding-right: 2px;
33 }
34
35 .arrow-right::after {
36 content: '';
37 }
38
39 .arrow-down::after {
40 content: '';
41 }
42
43 .toggle-arrow {
44 height: 100%;
45 width: 20px;
46 margin-top: 2px;
47 display: block;
48 cursor: pointer;
49 user-select: none;
50 }
51
52 #log-label {
53 padding-left: 18px;
54 background-position: left center;
55 background-repeat: no-repeat;
56 vertical-align: middle;
57 color: #15c;
58 user-select: none;
59 }
60
61 #content {
62 display: block;
63 position: relative;
64 width: 100%;
65 }
66
67 #inner-content {
68 display: block;
69 position: absolute;
70 width: 100%;
71 }
72
73 .content-bar {
74 display: block;
75 background-color: #f5f5f5;
76 padding: 0 5px 0 10px;
77 border-bottom: 1px solid #ebebeb;
78 text-align: right;
79 }
80
81 #wrapper {
82 overflow: scroll;
83 max-height: 250px;
84 display: block;
85 overflow: auto;
86 }
87
88 #logs {
89 width: 100%;
90 height: 100%;
91 border-bottom: 1px solid #e5e5e5;
92 border-collapse: collapse;
93 }
94
95 #logs tr {
96 border-bottom: 1px solid #e5e5e5;
97 }
98
99 #logs tr:hover {
100 background-color: #ffffd6
101 }
102
103 #logs td {
104 margin: 0;
105 padding: 0;
106 }
107
108 #logs tr td:first-child {
109 vertical-align: top;
110 text-align: left;
111 width: 23px;
112 }
113
114 #logs td .message {
115 position: relative;
116 height: 26px;
117 }
118
119 #logs td .message.expand {
120 height: auto !important;
121 }
122
123 #logs td .message pre {
124 position: absolute;
125 top: 0;
126 bottom: 0;
127 width: 100%;
128 margin: 0;
129 padding: 5px 0;
130 font-family: inherit;
131 overflow: hidden;
132 white-space: nowrap;
133 text-overflow: ellipsis;
134 }
135
136 /* Wraps text and also preserves line break.*/
137 #logs td .message.expand pre {
138 white-space: pre-line;
139 position: static;
140 height: auto !important;
141 }
142
143 .loading-img {
144 display: block;
145 margin-left: auto;
146 margin-right: auto;
147 }
148 </style>
149 <div id="container">
150
151 <div class="label-container">
152 <core-icon-button id="log-label" icon="expand-more" on-click="{{toggleView}}">
153 {{logLabel}}
154 </core-icon-button>
155 </div>
156
157 <div id="content" style="display:none">
158 <div id="inner-content">
159 <div class="content-bar">
160 <core-icon-button id="refresh-btn" icon="refresh" on-click="{{refresh}}">
161 </core-icon-button>
162 </div>
163 <div id="wrapper">
164 <table id="logs"></table>
165 <template bind if="{{stepLoading}}">
166 <img class="loading-img"
167 height="25"
168 width="25"
169 src="//www.google.com/images/loading.gif">
170 </template>
171 <template bind if="{{errorMessage}}">
172 <div class="error">{{errorMessage}}</div>
173 </template>
174 </div>
175 </div>
176 </div>
177 </div>
178 </template>
179 <script>
180 'use strict';
181 Polymer('quick-log', {
182
183 MAX_LOG_REQUEST_SIZE: 100,
184
185 /**
186 * Custom element lifecycle callback, called once this element is ready.
187 */
188 ready: function() {
189 this.logList = [];
190 this.xhr = null;
191 if (this.loadOnReady) {
192 this.getLogs();
193 if (this.expandOnReady) {
194 this.show();
195 }
196 }
197 },
198
199 /**
200 * Initializes log parameters and send a request to get logs.
201 * @param {string} logLabel The label of log handle for
202 * expanding log container.
203 * @param {string} logNamespace Namespace name.
204 * @param {string} logName Log name.
205 * @param {string} logFilter A regex string to filter logs.
206 */
207 initialize: function(logLabel, logNamespace, logName, logFilter) {
208 this.logLabel = logLabel;
209 this.logNamespace = logNamespace;
210 this.logName = logName;
211 this.logFilter = logFilter;
212 this.clear();
213 this.getLogs();
214 },
215
216 /**
217 * Sends XMLHttpRequest to get logs.
218 * @param {boolean} latest True to get the latest logs,
219 False to get older logs.
220 */
221 getLogs: function(latest) {
222 latest = ((latest == undefined) ? true : latest);
223 if (this.xhr) {
224 this.xhr.abort();
225 this.xhr = null;
226 }
227 this.setState('loading');
228 var params = {
229 namespace: this.logNamespace,
230 name: this.logName,
231 size: this.MAX_LOG_REQUEST_SIZE,
232 xsrf_token: this.xsrfToken
233 };
234 if (this.logFilter) {
235 params['filter'] = this.logFilter;
236 }
237 if (this.logList.length > 0) {
238 if (latest) {
239 params['after_timestamp'] = this.logList[0].timestamp;
240 } else {
241 var lastLog = this.logList[this.logList.length - 1];
242 params['before_timestamp'] = lastLog.timestamp;
243 }
244 }
245 this.xhr = simple_xhr.send('/get_logs', params,
246 function(logs) {
247 this.errorMessage = null;
248 this.setState('finished');
249 if (logs.length > 0) {
250 this.updateLogs(logs);
251 }
252 }.bind(this),
253 function(msg) {
254 this.errorMessage = msg;
255 this.setState('finished');
256 }.bind(this)
257 );
258 },
259
260 /**
261 * Updates current displaying logs with new logs.
262 * @param {Array.<Object>} newLogs Array of log objects.
263 */
264 updateLogs: function(newLogs) {
265 var insertBefore = true;
266 if (this.logList.length) {
267 var lastTimestamp = newLogs[newLogs.length - 1].timestamp;
268 insertBefore = lastTimestamp >= this.logList[0].timestamp;
269 }
270
271 var table = this.$.logs;
272 if (insertBefore) {
273 newLogs.reverse();
274 }
275 for (var i = 0; i < newLogs.length; i++) {
276 this.removeLog(table, newLogs[i]);
277 this.insertLog(table, newLogs[i], insertBefore);
278 }
279 this.updateHeight();
280 },
281
282 /**
283 * Inserts a log into HTML table.
284 * @param {Object} table Table HTML element.
285 * @param {Object} log A log object.
286 * @param {boolean} insertBefore true to prepend, false to append.
287 */
288 insertLog: function(table, log, insertBefore) {
289 if (insertBefore) {
290 this.logList.unshift(log);
291 } else {
292 this.logList.push(log);
293 }
294 var row = document.createElement('tr');
295 var expandTd = document.createElement('td');
296 row.appendChild(expandTd);
297 var span = document.createElement('span');
298 span.className = 'toggle-arrow arrow-right';
299 expandTd.appendChild(span);
300
301 var td = document.createElement('td');
302 var messageDiv = document.createElement('div');
303 messageDiv.className = 'message';
304 row.appendChild(td);
305 td.appendChild(messageDiv);
306 messageDiv.innerHTML = ''
+ log.message + '
';
307 span.onclick =
this.onLogToggleClick.bind(
this,
messageDiv);
308 table.insertBefore(
row,
table.childNodes[
0]);
309 },
310
311 /**
312 *
Removes a log.
313 * @
param {
Object}
table Table HTML element.
314 * @
param {
Object}
log A log object.
315 */
316 removeLog:
function(
table,
log) {
317 for (
var i = 0;
i <
this.logList.length; i++) {
318 if (
log.id ==
this.logList[i].id) {
319 this.logList.splice(i, 1);
320 table.deleteRow(i);
321 }
322 }
323 },
324
325 /**
326 * Toggles
show/
hide log.
327 */
328 onLogToggleClick: function(messageDiv, e) {
329 var arrowIcon =
e.target;
330 if (
arrowIcon.className.indexOf('arrow-right') > -1) {
331 arrowIcon.className = 'toggle-arrow arrow-down';
332 messageDiv.className = 'message expand';
333 } else {
334 arrowIcon.className = 'toggle-arrow arrow-right';
335 messageDiv.className = 'message';
336 }
337 this.updateHeight();
338 },
339
340 /**
341 * Specifies loading state.
342 */
343 setState: function(state) {
344 switch (state) {
345 case 'loading':
346 this.stepLoading = true;
347 this.$['refresh-btn'].disabled = true;
348 break;
349 case 'finished':
350 this.stepLoading = false;
351 this.$['refresh-btn'].disabled = false;
352 break;
353 }
354 },
355
356 /**
357 * Toggles
show/
hide log container.
358 */
359 toggleView: function() {
360 if (this.$
.content.style.display == '') {
361 this.hide();
362 } else {
363 this.show();
364 this.scrollIntoView();
365 }
366 },
367
368 /**
369 * Scrolls into view if log container is out of view.
370 */
371 scrollIntoView: function() {
372 var el = this.$.content;
373 var bottomOfPage =
window.pageYOffset +
window.innerHeight;
374 var bottomOfEl =
el.offsetTop +
el.offsetHeight;
375 if (bottomOfEl > bottomOfPage) {
376 el.scrollIntoView();
377 }
378 },
379
380 /**
381 * Refreshes log container.
382 */
383 refresh: function() {
384 if (
this.stepLoading) {
385 return;
386 }
387 this.getLogs();
388 },
389
390 /**
391 * Shows log container.
392 */
393 show: function() {
394 this.$['log-label'].icon = 'expand-less';
395 this.$
.content.style.display = '';
396 if (!
this.stepLoading) {
397 this.$['refresh-btn'].disabled = false;
398 }
399 this.updateHeight();
400 },
401
402 /**
403 * Hides log container.
404 */
405 hide: function() {
406 this.$['log-label'].icon = 'expand-more';
407 this.$
.content.style.display = 'none';
408 this.$['refresh-btn'].disabled = true;
409 },
410
411 /**
412 * Clear logs.
413 */
414 clear: function() {
415 this.logList = [];
416 this.$
.logs.innerHTML = '';
417 },
418
419 /**
420 * Since we use absolute inner div, we'll keep the parent div updated
421 * to make sure this element doesn't overlap with elements below.
422 */
423 updateHeight: function() {
424 this.$
.content.style.height = (
425 this.$['inner-content'].offsetHeight + 'px');
426 }
427 });
428 </
script>
429 </
polymer-
element>
430