github.com/c2s/go-ethereum@v1.9.7/dashboard/assets/components/Logs.jsx (about) 1 // @flow 2 3 // Copyright 2018 The go-ethereum Authors 4 // This file is part of the go-ethereum library. 5 // 6 // The go-ethereum library is free software: you can redistribute it and/or modify 7 // it under the terms of the GNU Lesser General Public License as published by 8 // the Free Software Foundation, either version 3 of the License, or 9 // (at your option) any later version. 10 // 11 // The go-ethereum library is distributed in the hope that it will be useful, 12 // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 // GNU Lesser General Public License for more details. 15 // 16 // You should have received a copy of the GNU Lesser General Public License 17 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 18 19 import React, {Component} from 'react'; 20 21 import List from '@material-ui/core/List'; 22 import ListItem from '@material-ui/core/ListItem'; 23 import escapeHtml from 'escape-html'; 24 import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content'; 25 26 // requestBand says how wide is the top/bottom zone, eg. 0.1 means 10% of the container height. 27 const requestBand = 0.05; 28 29 // fieldPadding is a global map with maximum field value lengths seen until now 30 // to allow padding log contexts in a bit smarter way. 31 const fieldPadding = new Map(); 32 33 // createChunk creates an HTML formatted object, which displays the given array similarly to 34 // the server side terminal. 35 const createChunk = (records: Array<Record>) => { 36 let content = ''; 37 records.forEach((record) => { 38 const {t, ctx} = record; 39 let {lvl, msg} = record; 40 let color = '#ce3c23'; 41 switch (lvl) { 42 case 'trace': 43 case 'trce': 44 lvl = 'TRACE'; 45 color = '#3465a4'; 46 break; 47 case 'debug': 48 case 'dbug': 49 lvl = 'DEBUG'; 50 color = '#3d989b'; 51 break; 52 case 'info': 53 lvl = 'INFO '; 54 color = '#4c8f0f'; 55 break; 56 case 'warn': 57 lvl = 'WARN '; 58 color = '#b79a22'; 59 break; 60 case 'error': 61 case 'eror': 62 lvl = 'ERROR'; 63 color = '#754b70'; 64 break; 65 case 'crit': 66 lvl = 'CRIT '; 67 color = '#ce3c23'; 68 break; 69 default: 70 lvl = ''; 71 } 72 const time = new Date(t); 73 if (lvl === '' || !(time instanceof Date) || isNaN(time) || typeof msg !== 'string' || !Array.isArray(ctx)) { 74 content += '<span style="color:#ce3c23">Invalid log record</span><br />'; 75 return; 76 } 77 if (ctx.length > 0) { 78 msg += ' '.repeat(Math.max(40 - msg.length, 0)); 79 } 80 const month = `0${time.getMonth() + 1}`.slice(-2); 81 const date = `0${time.getDate()}`.slice(-2); 82 const hours = `0${time.getHours()}`.slice(-2); 83 const minutes = `0${time.getMinutes()}`.slice(-2); 84 const seconds = `0${time.getSeconds()}`.slice(-2); 85 content += `<span style="color:${color}">${lvl}</span>[${month}-${date}|${hours}:${minutes}:${seconds}] ${msg}`; 86 87 for (let i = 0; i < ctx.length; i += 2) { 88 const key = escapeHtml(ctx[i]); 89 const val = escapeHtml(ctx[i + 1]); 90 let padding = fieldPadding.get(key); 91 if (typeof padding !== 'number' || padding < val.length) { 92 padding = val.length; 93 fieldPadding.set(key, padding); 94 } 95 let p = ''; 96 if (i < ctx.length - 2) { 97 p = ' '.repeat(padding - val.length); 98 } 99 content += ` <span style="color:${color}">${key}</span>=${val}${p}`; 100 } 101 content += '<br />'; 102 }); 103 return content; 104 }; 105 106 // ADDED, SAME and REMOVED are used to track the change of the log chunk array. 107 // The scroll position is set using these values. 108 export const ADDED = 1; 109 export const SAME = 0; 110 export const REMOVED = -1; 111 112 // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. 113 // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. 114 export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => { 115 prev.topChanged = SAME; 116 prev.bottomChanged = SAME; 117 if (!Array.isArray(update.chunk) || update.chunk.length < 1) { 118 return prev; 119 } 120 if (!Array.isArray(prev.chunks)) { 121 prev.chunks = []; 122 } 123 const content = createChunk(update.chunk); 124 if (!update.source) { 125 // In case of stream chunk. 126 if (!prev.endBottom) { 127 return prev; 128 } 129 if (prev.chunks.length < 1) { 130 // This should never happen, because the first chunk is always a non-stream chunk. 131 return [{content, name: '00000000000000.log'}]; 132 } 133 prev.chunks[prev.chunks.length - 1].content += content; 134 prev.bottomChanged = ADDED; 135 return prev; 136 } 137 const chunk = { 138 content, 139 name: update.source.name, 140 }; 141 if (prev.chunks.length > 0 && update.source.name < prev.chunks[0].name) { 142 if (update.source.last) { 143 prev.endTop = true; 144 } 145 if (prev.chunks.length >= limit) { 146 prev.endBottom = false; 147 prev.chunks.splice(limit - 1, prev.chunks.length - limit + 1); 148 prev.bottomChanged = REMOVED; 149 } 150 prev.chunks = [chunk, ...prev.chunks]; 151 prev.topChanged = ADDED; 152 return prev; 153 } 154 if (update.source.last) { 155 prev.endBottom = true; 156 } 157 if (prev.chunks.length >= limit) { 158 prev.endTop = false; 159 prev.chunks.splice(0, prev.chunks.length - limit + 1); 160 prev.topChanged = REMOVED; 161 } 162 prev.chunks = [...prev.chunks, chunk]; 163 prev.bottomChanged = ADDED; 164 return prev; 165 }; 166 167 // styles contains the constant styles of the component. 168 const styles = { 169 logListItem: { 170 padding: 0, 171 lineHeight: 1.231, 172 }, 173 logChunk: { 174 color: 'white', 175 fontFamily: 'monospace', 176 whiteSpace: 'nowrap', 177 width: 0, 178 }, 179 waitMsg: { 180 textAlign: 'center', 181 color: 'white', 182 fontFamily: 'monospace', 183 }, 184 }; 185 186 export type Props = { 187 container: Object, 188 content: Content, 189 shouldUpdate: Object, 190 send: string => void, 191 }; 192 193 type State = { 194 requestAllowed: boolean, 195 }; 196 197 // Logs renders the log page. 198 class Logs extends Component<Props, State> { 199 constructor(props: Props) { 200 super(props); 201 this.content = React.createRef(); 202 this.state = { 203 requestAllowed: true, 204 }; 205 } 206 207 componentDidMount() { 208 const {container} = this.props; 209 if (typeof container === 'undefined') { 210 return; 211 } 212 container.scrollTop = container.scrollHeight - container.clientHeight; 213 const {logs} = this.props.content; 214 if (typeof this.content === 'undefined' || logs.chunks.length < 1) { 215 return; 216 } 217 if (this.content.clientHeight < container.clientHeight && !logs.endTop) { 218 this.sendRequest(logs.chunks[0].name, true); 219 } 220 } 221 222 // onScroll is triggered by the parent component's scroll event, and sends requests if the scroll position is 223 // at the top or at the bottom. 224 onScroll = () => { 225 if (!this.state.requestAllowed || typeof this.content === 'undefined') { 226 return; 227 } 228 const {logs} = this.props.content; 229 if (logs.chunks.length < 1) { 230 return; 231 } 232 if (this.atTop() && !logs.endTop) { 233 this.sendRequest(logs.chunks[0].name, true); 234 } else if (this.atBottom() && !logs.endBottom) { 235 this.sendRequest(logs.chunks[logs.chunks.length - 1].name, false); 236 } 237 }; 238 239 sendRequest = (name: string, past: boolean) => { 240 this.setState({requestAllowed: false}); 241 this.props.send(JSON.stringify({ 242 Logs: { 243 Name: name, 244 Past: past, 245 }, 246 })); 247 }; 248 249 // atTop checks if the scroll position it at the top of the container. 250 atTop = () => this.props.container.scrollTop <= this.props.container.scrollHeight * requestBand; 251 252 // atBottom checks if the scroll position it at the bottom of the container. 253 atBottom = () => { 254 const {container} = this.props; 255 return container.scrollHeight - container.scrollTop 256 <= container.clientHeight + container.scrollHeight * requestBand; 257 }; 258 259 // beforeUpdate is called by the parent component, saves the previous scroll position 260 // and the height of the first log chunk, which can be deleted during the insertion. 261 beforeUpdate = () => { 262 let firstHeight = 0; 263 const chunkList = this.content.children[1]; 264 if (chunkList && chunkList.children[0]) { 265 firstHeight = chunkList.children[0].clientHeight; 266 } 267 return { 268 scrollTop: this.props.container.scrollTop, 269 firstHeight, 270 }; 271 }; 272 273 // didUpdate is called by the parent component, which provides the container. Sends the first request if the 274 // visible part of the container isn't full, and resets the scroll position in order to avoid jumping when a 275 // chunk is inserted or removed. 276 didUpdate = (prevProps, prevState, snapshot) => { 277 if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined' || snapshot === null) { 278 return; 279 } 280 const {logs} = this.props.content; 281 const {container} = this.props; 282 if (typeof container === 'undefined' || logs.chunks.length < 1) { 283 return; 284 } 285 if (this.content.clientHeight < container.clientHeight) { 286 // Only enters here at the beginning, when there aren't enough logs to fill the container 287 // and the scroll bar doesn't appear. 288 if (!logs.endTop) { 289 this.sendRequest(logs.chunks[0].name, true); 290 } 291 return; 292 } 293 let {scrollTop} = snapshot; 294 if (logs.topChanged === ADDED) { 295 // It would be safer to use a ref to the list, but ref doesn't work well with HOCs. 296 scrollTop += this.content.children[1].children[0].clientHeight; 297 } else if (logs.bottomChanged === ADDED) { 298 if (logs.topChanged === REMOVED) { 299 scrollTop -= snapshot.firstHeight; 300 } else if (this.atBottom() && logs.endBottom) { 301 scrollTop = container.scrollHeight - container.clientHeight; 302 } 303 } 304 container.scrollTop = scrollTop; 305 this.setState({requestAllowed: true}); 306 }; 307 308 render() { 309 return ( 310 <div ref={(ref) => { this.content = ref; }}> 311 <div style={styles.waitMsg}> 312 {this.props.content.logs.endTop ? 'No more logs.' : 'Waiting for server...'} 313 </div> 314 <List> 315 {this.props.content.logs.chunks.map((c, index) => ( 316 <ListItem style={styles.logListItem} key={index}> 317 <div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} /> 318 </ListItem> 319 ))} 320 </List> 321 {this.props.content.logs.endBottom || <div style={styles.waitMsg}>Waiting for server...</div>} 322 </div> 323 ); 324 } 325 } 326 327 export default Logs;