Home | History | Annotate | Download | only in elements
      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