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