diff options
author | Kurkó Mihály <kurkomisi@users.noreply.github.com> | 2018-07-11 15:59:04 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2018-07-11 15:59:04 +0800 |
commit | a9835c1816bc49ee54c82b4f2a5b05cbcd89881b (patch) | |
tree | e1badefd627aa3a7c4e1937eab22b8fe3eb204d1 /dashboard/assets/components/Logs.jsx | |
parent | 2eedbe799f5eb8766e4808d8a1810cc1c90c4b93 (diff) | |
download | go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.tar go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.tar.gz go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.tar.bz2 go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.tar.lz go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.tar.xz go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.tar.zst go-tangerine-a9835c1816bc49ee54c82b4f2a5b05cbcd89881b.zip |
cmd, dashboard, log: log collection and exploration (#17097)
* cmd, dashboard, internal, log, node: logging feature
* cmd, dashboard, internal, log: requested changes
* dashboard, vendor: gofmt, govendor, use vendored file watcher
* dashboard, log: gofmt -s -w, goimports
* dashboard, log: gosimple
Diffstat (limited to 'dashboard/assets/components/Logs.jsx')
-rw-r--r-- | dashboard/assets/components/Logs.jsx | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/dashboard/assets/components/Logs.jsx b/dashboard/assets/components/Logs.jsx new file mode 100644 index 000000000..203014276 --- /dev/null +++ b/dashboard/assets/components/Logs.jsx @@ -0,0 +1,310 @@ +// @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; |