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