| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- // @flow
- // Copyright 2018 The go-ethereum Authors
- // This file is part of the go-ethereum library.
- //
- // The go-ethereum library is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Lesser General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // The go-ethereum library is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Lesser General Public License for more details.
- //
- // You should have received a copy of the GNU Lesser General Public License
- // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
- import React, {Component} from 'react';
- import List, {ListItem} from 'material-ui/List';
- import type {Record, Content, LogsMessage, Logs as LogsType} from '../types/content';
- // requestBand says how wide is the top/bottom zone, eg. 0.1 means 10% of the container height.
- const requestBand = 0.05;
- // fieldPadding is a global map with maximum field value lengths seen until now
- // to allow padding log contexts in a bit smarter way.
- const fieldPadding = new Map();
- // createChunk creates an HTML formatted object, which displays the given array similarly to
- // the server side terminal.
- const createChunk = (records: Array<Record>) => {
- let content = '';
- records.forEach((record) => {
- const {t, ctx} = record;
- let {lvl, msg} = record;
- let color = '#ce3c23';
- switch (lvl) {
- case 'trace':
- case 'trce':
- lvl = 'TRACE';
- color = '#3465a4';
- break;
- case 'debug':
- case 'dbug':
- lvl = 'DEBUG';
- color = '#3d989b';
- break;
- case 'info':
- lvl = 'INFO ';
- color = '#4c8f0f';
- break;
- case 'warn':
- lvl = 'WARN ';
- color = '#b79a22';
- break;
- case 'error':
- case 'eror':
- lvl = 'ERROR';
- color = '#754b70';
- break;
- case 'crit':
- lvl = 'CRIT ';
- color = '#ce3c23';
- break;
- default:
- lvl = '';
- }
- const time = new Date(t);
- if (lvl === '' || !(time instanceof Date) || isNaN(time) || typeof msg !== 'string' || !Array.isArray(ctx)) {
- content += '<span style="color:#ce3c23">Invalid log record</span><br />';
- return;
- }
- if (ctx.length > 0) {
- msg += ' '.repeat(Math.max(40 - msg.length, 0));
- }
- const month = `0${time.getMonth() + 1}`.slice(-2);
- const date = `0${time.getDate()}`.slice(-2);
- const hours = `0${time.getHours()}`.slice(-2);
- const minutes = `0${time.getMinutes()}`.slice(-2);
- const seconds = `0${time.getSeconds()}`.slice(-2);
- content += `<span style="color:${color}">${lvl}</span>[${month}-${date}|${hours}:${minutes}:${seconds}] ${msg}`;
- for (let i = 0; i < ctx.length; i += 2) {
- const key = ctx[i];
- const val = ctx[i + 1];
- let padding = fieldPadding.get(key);
- if (typeof padding !== 'number' || padding < val.length) {
- padding = val.length;
- fieldPadding.set(key, padding);
- }
- let p = '';
- if (i < ctx.length - 2) {
- p = ' '.repeat(padding - val.length);
- }
- content += ` <span style="color:${color}">${key}</span>=${val}${p}`;
- }
- content += '<br />';
- });
- return content;
- };
- // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array.
- // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM.
- export const inserter = (limit: number) => (update: LogsMessage, prev: LogsType) => {
- prev.topChanged = 0;
- prev.bottomChanged = 0;
- if (!Array.isArray(update.chunk) || update.chunk.length < 1) {
- return prev;
- }
- if (!Array.isArray(prev.chunks)) {
- prev.chunks = [];
- }
- const content = createChunk(update.chunk);
- if (!update.source) {
- // In case of stream chunk.
- if (!prev.endBottom) {
- return prev;
- }
- if (prev.chunks.length < 1) {
- // This should never happen, because the first chunk is always a non-stream chunk.
- return [{content, name: '00000000000000.log'}];
- }
- prev.chunks[prev.chunks.length - 1].content += content;
- prev.bottomChanged = 1;
- return prev;
- }
- const chunk = {
- content,
- name: update.source.name,
- };
- if (prev.chunks.length > 0 && update.source.name < prev.chunks[0].name) {
- if (update.source.last) {
- prev.endTop = true;
- }
- if (prev.chunks.length >= limit) {
- prev.endBottom = false;
- prev.chunks.splice(limit - 1, prev.chunks.length - limit + 1);
- prev.bottomChanged = -1;
- }
- prev.chunks = [chunk, ...prev.chunks];
- prev.topChanged = 1;
- return prev;
- }
- if (update.source.last) {
- prev.endBottom = true;
- }
- if (prev.chunks.length >= limit) {
- prev.endTop = false;
- prev.chunks.splice(0, prev.chunks.length - limit + 1);
- prev.topChanged = -1;
- }
- prev.chunks = [...prev.chunks, chunk];
- prev.bottomChanged = 1;
- return prev;
- };
- // styles contains the constant styles of the component.
- const styles = {
- logListItem: {
- padding: 0,
- },
- logChunk: {
- color: 'white',
- fontFamily: 'monospace',
- whiteSpace: 'nowrap',
- width: 0,
- },
- };
- export type Props = {
- container: Object,
- content: Content,
- shouldUpdate: Object,
- send: string => void,
- };
- type State = {
- requestAllowed: boolean,
- };
- // Logs renders the log page.
- class Logs extends Component<Props, State> {
- constructor(props: Props) {
- super(props);
- this.content = React.createRef();
- this.state = {
- requestAllowed: true,
- };
- }
- componentDidMount() {
- const {container} = this.props;
- container.scrollTop = container.scrollHeight - container.clientHeight;
- }
- // onScroll is triggered by the parent component's scroll event, and sends requests if the scroll position is
- // at the top or at the bottom.
- onScroll = () => {
- if (!this.state.requestAllowed || typeof this.content === 'undefined') {
- return;
- }
- const {logs} = this.props.content;
- if (logs.chunks.length < 1) {
- return;
- }
- if (this.atTop()) {
- if (!logs.endTop) {
- this.setState({requestAllowed: false});
- this.props.send(JSON.stringify({
- Logs: {
- Name: logs.chunks[0].name,
- Past: true,
- },
- }));
- }
- } else if (this.atBottom()) {
- if (!logs.endBottom) {
- this.setState({requestAllowed: false});
- this.props.send(JSON.stringify({
- Logs: {
- Name: logs.chunks[logs.chunks.length - 1].name,
- Past: false,
- },
- }));
- }
- }
- };
- // atTop checks if the scroll position it at the top of the container.
- atTop = () => this.props.container.scrollTop <= this.props.container.scrollHeight * requestBand;
- // atBottom checks if the scroll position it at the bottom of the container.
- atBottom = () => {
- const {container} = this.props;
- return container.scrollHeight - container.scrollTop <=
- container.clientHeight + container.scrollHeight * requestBand;
- };
- // beforeUpdate is called by the parent component, saves the previous scroll position
- // and the height of the first log chunk, which can be deleted during the insertion.
- beforeUpdate = () => {
- let firstHeight = 0;
- if (this.content && this.content.children[0] && this.content.children[0].children[0]) {
- firstHeight = this.content.children[0].children[0].clientHeight;
- }
- return {
- scrollTop: this.props.container.scrollTop,
- firstHeight,
- };
- };
- // didUpdate is called by the parent component, which provides the container. Sends the first request if the
- // visible part of the container isn't full, and resets the scroll position in order to avoid jumping when new
- // chunk is inserted.
- didUpdate = (prevProps, prevState, snapshot) => {
- if (typeof this.props.shouldUpdate.logs === 'undefined' || typeof this.content === 'undefined' || snapshot === null) {
- return;
- }
- const {logs} = this.props.content;
- const {container} = this.props;
- if (typeof container === 'undefined' || logs.chunks.length < 1) {
- return;
- }
- if (this.content.clientHeight < container.clientHeight) {
- // Only enters here at the beginning, when there isn't enough log to fill the container
- // and the scroll bar doesn't appear.
- if (!logs.endTop) {
- this.setState({requestAllowed: false});
- this.props.send(JSON.stringify({
- Logs: {
- Name: logs.chunks[0].name,
- Past: true,
- },
- }));
- }
- return;
- }
- const chunks = this.content.children[0].children;
- let {scrollTop} = snapshot;
- if (logs.topChanged > 0) {
- scrollTop += chunks[0].clientHeight;
- } else if (logs.bottomChanged > 0) {
- if (logs.topChanged < 0) {
- scrollTop -= snapshot.firstHeight;
- } else if (logs.endBottom && this.atBottom()) {
- scrollTop = container.scrollHeight - container.clientHeight;
- }
- }
- container.scrollTop = scrollTop;
- this.setState({requestAllowed: true});
- };
- render() {
- return (
- <div ref={(ref) => { this.content = ref; }}>
- <List>
- {this.props.content.logs.chunks.map((c, index) => (
- <ListItem style={styles.logListItem} key={index}>
- <div style={styles.logChunk} dangerouslySetInnerHTML={{__html: c.content}} />
- </ListItem>
- ))}
- </List>
- </div>
- );
- }
- }
- export default Logs;
|