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&nbsp;';
    54  			color = '#4c8f0f';
    55  			break;
    56  		case 'warn':
    57  			lvl = 'WARN&nbsp;';
    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&nbsp;';
    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 += '&nbsp;'.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 = '&nbsp;'.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;