1// Copyright 2022 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15var VirtualizedList = window.VirtualizedList.default; 16const rowHeight = 30; 17 18function formatDate(dt) { 19 function pad2(n) { 20 return (n < 10 ? '0' : '') + n; 21 } 22 23 return dt.getFullYear() + pad2(dt.getMonth() + 1) + pad2(dt.getDate()) + ' ' + 24 pad2(dt.getHours()) + ':' + pad2(dt.getMinutes()) + ':' + 25 pad2(dt.getSeconds()); 26} 27 28let data = []; 29function clearLogs() { 30 data = [{ 31 'message': 'Logs started', 32 'levelno': 20, 33 time: formatDate(new Date()), 34 'levelname': '\u001b[35m\u001b[1mINF\u001b[0m', 35 'args': [], 36 'fields': {'module': '', 'file': '', 'timestamp': '', 'keys': ''} 37 }]; 38} 39clearLogs(); 40 41let nonAdditionalDataFields = 42 ['_hosttime', 'levelname', 'levelno', 'args', 'fields', 'message', 'time']; 43let additionalHeaders = []; 44function updateHeadersFromData(data) { 45 let dirty = false; 46 Object.keys(data).forEach((columnName) => { 47 if (nonAdditionalDataFields.indexOf(columnName) === -1 && 48 additionalHeaders.indexOf(columnName) === -1) { 49 additionalHeaders.push(columnName); 50 dirty = true; 51 } 52 }); 53 Object.keys(data.fields || {}).forEach((columnName) => { 54 if (nonAdditionalDataFields.indexOf(columnName) === -1 && 55 additionalHeaders.indexOf(columnName) === -1) { 56 additionalHeaders.push(columnName); 57 dirty = true; 58 } 59 }); 60 61 const headerDOM = document.querySelector('.log-header'); 62 if (dirty) { 63 headerDOM.innerHTML = ` 64 <span class="_hosttime">Time</span> 65 <span class="level">Level</span> 66 ${ 67 additionalHeaders 68 .map((key) => ` 69 <span class="${key}">${key}</span> 70 `).join('\n')} 71 <span class="msg">Message</span>` 72 } 73 74 // Also update column widths to match actual row. 75 const headerChildren = Array.from(headerDOM.children); 76 77 const firstRow = document.querySelector('.log-container .log-entry'); 78 const firstRowChildren = Array.from(firstRow.children); 79 headerChildren.forEach((col, index) => { 80 if (firstRowChildren[index]) { 81 col.setAttribute( 82 'style', 83 `width:${firstRowChildren[index].getBoundingClientRect().width}`); 84 col.setAttribute('title', col.innerText); 85 } 86 }) 87} 88 89function getUrlHashParameter(param) { 90 var params = getUrlHashParameters(); 91 return params[param]; 92} 93 94function getUrlHashParameters() { 95 var sPageURL = window.location.hash; 96 if (sPageURL) 97 sPageURL = sPageURL.split('#')[1]; 98 var pairs = sPageURL.split('&'); 99 var object = {}; 100 pairs.forEach(function(pair, i) { 101 pair = pair.split('='); 102 if (pair[0] !== '') 103 object[pair[0]] = pair[1]; 104 }); 105 return object; 106} 107let currentTheme = {}; 108let defaultLogStyleRule = 'color: #ffffff;'; 109let columnStyleRules = {}; 110let defaultColumnStyles = []; 111let logLevelStyles = {}; 112const logLevelToString = { 113 10: 'DBG', 114 20: 'INF', 115 21: 'OUT', 116 30: 'WRN', 117 40: 'ERR', 118 50: 'CRT', 119 70: 'FTL' 120}; 121 122function setCurrentTheme(newTheme) { 123 currentTheme = newTheme; 124 defaultLogStyleRule = parseStyle(newTheme.default); 125 document.querySelector('body').setAttribute('style', defaultLogStyleRule); 126 // Apply default font styles to columns 127 let styles = []; 128 Object.keys(newTheme).forEach(key => { 129 if (key.startsWith('log-table-column-')) { 130 styles.push(newTheme[key]); 131 } 132 if (key.startsWith('log-level-')) { 133 logLevelStyles[parseInt(key.replace('log-level-', ''))] = 134 parseStyle(newTheme[key]); 135 } 136 }); 137 defaultColumnStyles = styles; 138} 139 140function parseStyle(rule) { 141 const ruleList = rule.split(' '); 142 let outputStyle = ruleList.map(fragment => { 143 if (fragment.startsWith('bg:')) { 144 return `background-color: ${fragment.replace('bg:', '')}` 145 } else if (fragment === 'bold') { 146 return `font-weight: bold`; 147 } else if (fragment === 'underline') { 148 return `text-decoration: underline`; 149 } else if (fragment.startsWith('#')) { 150 return `color: ${fragment}`; 151 } 152 }); 153 return outputStyle.join(';') 154} 155 156function applyStyling(data, applyColors = false) { 157 let colIndex = 0; 158 Object.keys(data).forEach(key => { 159 if (columnStyleRules[key] && typeof data[key] === 'string') { 160 Object.keys(columnStyleRules[key]).forEach(token => { 161 data[key] = data[key].replaceAll( 162 token, 163 `<span 164 style="${defaultLogStyleRule};${ 165 applyColors ? (defaultColumnStyles 166 [colIndex % defaultColumnStyles.length]) : 167 ''};${parseStyle(columnStyleRules[key][token])};"> 168 ${token} 169 </span>`); 170 }); 171 } else if (key === 'fields') { 172 data[key] = applyStyling(data.fields, true); 173 } 174 if (applyColors) { 175 data[key] = `<span 176 style="${ 177 parseStyle( 178 defaultColumnStyles[colIndex % defaultColumnStyles.length])}"> 179 ${data[key]} 180 </span>`; 181 } 182 colIndex++; 183 }); 184 return data; 185} 186 187(function() { 188const container = document.querySelector('.log-container'); 189const height = window.innerHeight - 50 190let follow = true; 191// Initialize our VirtualizedList 192var virtualizedList = new VirtualizedList(container, { 193 height, 194 rowCount: data.length, 195 rowHeight: rowHeight, 196 estimatedRowHeight: rowHeight, 197 renderRow: (index) => { 198 const element = document.createElement('div'); 199 element.classList.add('log-entry'); 200 element.setAttribute('style', `height: ${rowHeight}px;`); 201 const logData = data[index]; 202 element.innerHTML = ` 203 <span class="time">${logData.time}</span> 204 <span class="level" style="${logLevelStyles[logData.levelno] || ''}">${ 205 logLevelToString[logData.levelno]}</span> 206 ${ 207 additionalHeaders 208 .map( 209 (key) => ` 210 <span class="${key}">${ 211 logData[key] || logData.fields[key] || ''}</span> 212 `).join('\n')} 213 <span class="msg">${logData.message}</span> 214 `; 215 return element; 216 }, 217 initialIndex: 0, 218 onScroll: (scrollTop, event) => { 219 const offset = 220 virtualizedList._sizeAndPositionManager.getUpdatedOffsetForIndex({ 221 containerSize: height, 222 targetIndex: data.length - 1, 223 }); 224 225 if (scrollTop < offset) { 226 follow = false; 227 } else { 228 follow = true; 229 } 230 } 231}); 232 233const port = getUrlHashParameter('ws') 234const hostname = location.hostname || '127.0.0.1'; 235var ws = new WebSocket(`ws://${hostname}:${port}/`); 236ws.onmessage = function(event) { 237 let dataObj; 238 try { 239 dataObj = JSON.parse(event.data); 240 } catch (e) { 241 } 242 if (!dataObj) 243 return; 244 245 if (dataObj.__pw_console_colors) { 246 const colors = dataObj.__pw_console_colors; 247 setCurrentTheme(colors.classes); 248 if (colors.column_values) { 249 columnStyleRules = {...colors.column_values}; 250 } 251 } else { 252 const currentData = {...dataObj, time: formatDate(new Date())}; 253 updateHeadersFromData(currentData); 254 data.push(applyStyling(currentData)); 255 virtualizedList.setRowCount(data.length); 256 if (follow) { 257 virtualizedList.scrollToIndex(data.length - 1); 258 } 259 } 260}; 261})(); 262