| <!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> |