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