1<!DOCTYPE html>
2<!--
3Copyright 2016 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found 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 = '<pre>' + log.message + '</pre>';
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