github.com/oskarth/go-ethereum@v1.6.8-0.20191013093314-dac24a9d3494/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&nbsp;';
    53  			color = '#4c8f0f';
    54  			break;
    55  		case 'warn':
    56  			lvl = 'WARN&nbsp;';
    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&nbsp;';
    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 += '&nbsp;'.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 = '&nbsp;'.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;