mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 09:23:48 +00:00
dashboard: memory, traffic and CPU on footer
This commit is contained in:
parent
7ba7418658
commit
bd03f0a0ca
20 changed files with 1854 additions and 1232 deletions
1715
dashboard/assets.go
1715
dashboard/assets.go
File diff suppressed because one or more lines are too long
|
|
@ -40,6 +40,9 @@
|
||||||
'react/jsx-indent': ['error', 'tab'],
|
'react/jsx-indent': ['error', 'tab'],
|
||||||
'react/jsx-indent-props': ['error', 'tab'],
|
'react/jsx-indent-props': ['error', 'tab'],
|
||||||
'react/prefer-stateless-function': 'off',
|
'react/prefer-stateless-function': 'off',
|
||||||
|
'jsx-quotes': ['error', 'prefer-single'],
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-console': ['error', { allow: ['error'] }],
|
||||||
|
|
||||||
// Specifies the maximum length of a line.
|
// Specifies the maximum length of a line.
|
||||||
'max-len': ['warn', 120, 2, {
|
'max-len': ['warn', 120, 2, {
|
||||||
|
|
@ -49,7 +52,7 @@
|
||||||
'ignoreStrings': true,
|
'ignoreStrings': true,
|
||||||
'ignoreTemplateLiterals': true,
|
'ignoreTemplateLiterals': true,
|
||||||
}],
|
}],
|
||||||
// Enforces spacing between keys and values in object literal properties.
|
// Enforces consistent spacing between keys and values in object literal properties.
|
||||||
'key-spacing': ['error', {'align': {
|
'key-spacing': ['error', {'align': {
|
||||||
'beforeColon': false,
|
'beforeColon': false,
|
||||||
'afterColon': true,
|
'afterColon': true,
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,9 @@ export type MenuProp = {|...ProvidedMenuProp, id: string|};
|
||||||
export const MENU: Map<string, {...MenuProp}> = new Map(menuSkeletons.map(({id, menu}) => ([id, {id, ...menu}])));
|
export const MENU: Map<string, {...MenuProp}> = new Map(menuSkeletons.map(({id, menu}) => ([id, {id, ...menu}])));
|
||||||
|
|
||||||
export const DURATION = 200;
|
export const DURATION = 200;
|
||||||
|
|
||||||
|
export const styles = {
|
||||||
|
light: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.54)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -18,35 +18,32 @@
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
import withStyles from 'material-ui/styles/withStyles';
|
|
||||||
|
|
||||||
import SideBar from './SideBar';
|
import SideBar from './SideBar';
|
||||||
import Main from './Main';
|
import Main from './Main';
|
||||||
import type {Content} from '../types/content';
|
import type {Content} from '../types/content';
|
||||||
|
|
||||||
// Styles for the Body component.
|
// styles contains the constant styles of the component.
|
||||||
const styles = () => ({
|
const styles = {
|
||||||
body: {
|
body: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
classes: Object,
|
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
changeContent: () => {},
|
changeContent: string => void,
|
||||||
active: string,
|
active: string,
|
||||||
content: Content,
|
content: Content,
|
||||||
shouldUpdate: Object,
|
shouldUpdate: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Body renders the body of the dashboard.
|
// Body renders the body of the dashboard.
|
||||||
class Body extends Component<Props> {
|
class Body extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {classes} = this.props; // The classes property is injected by withStyles().
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.body}>
|
<div style={styles.body}>
|
||||||
<SideBar
|
<SideBar
|
||||||
opened={this.props.opened}
|
opened={this.props.opened}
|
||||||
changeContent={this.props.changeContent}
|
changeContent={this.props.changeContent}
|
||||||
|
|
@ -61,4 +58,4 @@ class Body extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Body);
|
export default Body;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class ChartGrid extends Component<Props> {
|
||||||
{
|
{
|
||||||
React.Children.map(this.props.children, child => (
|
React.Children.map(this.props.children, child => (
|
||||||
<Grid item xs={child.props.xs}>
|
<Grid item xs={child.props.xs}>
|
||||||
<ResponsiveContainer width="100%" height={child.props.height}>
|
<ResponsiveContainer width='100%' height={child.props.height}>
|
||||||
{React.cloneElement(child, {data: child.props.values.map(value => ({value}))})}
|
{React.cloneElement(child, {data: child.props.values.map(value => ({value}))})}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
57
dashboard/assets/components/ChartRow.jsx
Normal file
57
dashboard/assets/components/ChartRow.jsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// @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 type {ChildrenArray} from 'react';
|
||||||
|
|
||||||
|
import Grid from 'material-ui/Grid';
|
||||||
|
|
||||||
|
// styles contains the constant styles of the component.
|
||||||
|
const styles = {
|
||||||
|
container: {
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
height: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
children: ChildrenArray<React$Element<any>>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ChartRow renders a row of equally sized responsive charts.
|
||||||
|
class ChartRow extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Grid container direction='row' style={styles.container} justify='space-between'>
|
||||||
|
{React.Children.map(this.props.children, child => (
|
||||||
|
<Grid item xs style={styles.item}>
|
||||||
|
{child}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChartRow;
|
||||||
75
dashboard/assets/components/CustomTooltip.jsx
Normal file
75
dashboard/assets/components/CustomTooltip.jsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
// @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 Typography from 'material-ui/Typography';
|
||||||
|
import {styles} from '../common';
|
||||||
|
|
||||||
|
// multiplier multiplies a number by another.
|
||||||
|
export const multiplier = <T>(by: number = 1) => (x: number) => x * by;
|
||||||
|
|
||||||
|
// valuePlotter renders a tooltip, which shows the value of the payload.
|
||||||
|
export const valuePlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => {
|
||||||
|
const p = mapper(payload);
|
||||||
|
return (
|
||||||
|
<Typography type='caption' color='inherit'>
|
||||||
|
<span style={styles.light}>{text}</span> {p}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// unit contains the units for the bytePlotter.
|
||||||
|
const unit = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
// bytePlotter renders a tooltip, which shows the simplified byte value of the payload followed by the unit.
|
||||||
|
export const bytePlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => {
|
||||||
|
let p = mapper(payload);
|
||||||
|
if (typeof p !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (; p > 1024 && i < 5; i++) {
|
||||||
|
p /= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography type='caption' color='inherit'>
|
||||||
|
<span style={styles.light}>{text}</span> {p.toFixed(2)} {unit[i]}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
active: boolean,
|
||||||
|
payload: Object,
|
||||||
|
tooltip: <T>(text: string, mapper?: T => T) => (payload: mixed) => null | React$Element<any>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// CustomTooltip takes a tooltip function, and uses it to plot the active value of the chart.
|
||||||
|
class CustomTooltip extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
const {active, payload, tooltip} = this.props;
|
||||||
|
if (!active || typeof tooltip !== 'function') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return tooltip(payload[0].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomTooltip;
|
||||||
|
|
@ -22,8 +22,7 @@ import withStyles from 'material-ui/styles/withStyles';
|
||||||
|
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Body from './Body';
|
import Body from './Body';
|
||||||
import Footer from './Footer';
|
import {MENU} from '../common';
|
||||||
import {MENU} from './Common';
|
|
||||||
import type {Content} from '../types/content';
|
import type {Content} from '../types/content';
|
||||||
|
|
||||||
// deepUpdate updates an object corresponding to the given update data, which has
|
// deepUpdate updates an object corresponding to the given update data, which has
|
||||||
|
|
@ -35,17 +34,17 @@ import type {Content} from '../types/content';
|
||||||
// the generalization of the message handling. The only necessary thing is to set a
|
// the generalization of the message handling. The only necessary thing is to set a
|
||||||
// handler function for every path of the state in order to maximize the flexibility
|
// handler function for every path of the state in order to maximize the flexibility
|
||||||
// of the update.
|
// of the update.
|
||||||
const deepUpdate = (prev: Object, update: Object, updater: Object) => {
|
const deepUpdate = (updater: Object, update: Object, prev: Object): $Shape<Content> => {
|
||||||
if (typeof update === 'undefined') {
|
if (typeof update === 'undefined') {
|
||||||
// TODO (kurkomisi): originally this was deep copy, investigate it.
|
// TODO (kurkomisi): originally this was deep copy, investigate it.
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
if (typeof updater === 'function') {
|
if (typeof updater === 'function') {
|
||||||
return updater(prev, update);
|
return updater(update, prev);
|
||||||
}
|
}
|
||||||
const updated = {};
|
const updated = {};
|
||||||
Object.keys(prev).forEach((key) => {
|
Object.keys(prev).forEach((key) => {
|
||||||
updated[key] = deepUpdate(prev[key], update[key], updater[key]);
|
updated[key] = deepUpdate(updater[key], update[key], prev[key]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -56,21 +55,25 @@ const deepUpdate = (prev: Object, update: Object, updater: Object) => {
|
||||||
// whether the involved data was changed or not by checking the message structure.
|
// whether the involved data was changed or not by checking the message structure.
|
||||||
//
|
//
|
||||||
// We could return the message itself too, but it's safer not to give access to it.
|
// We could return the message itself too, but it's safer not to give access to it.
|
||||||
const shouldUpdate = (msg: Object, updater: Object) => {
|
const shouldUpdate = (updater: Object, msg: Object) => {
|
||||||
const su = {};
|
const su = {};
|
||||||
Object.keys(msg).forEach((key) => {
|
Object.keys(msg).forEach((key) => {
|
||||||
su[key] = typeof updater[key] !== 'function' ? shouldUpdate(msg[key], updater[key]) : true;
|
su[key] = typeof updater[key] !== 'function' ? shouldUpdate(updater[key], msg[key]) : true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return su;
|
return su;
|
||||||
};
|
};
|
||||||
|
|
||||||
// appender is a state update generalization function, which appends the update data
|
// replacer is a state updater function, which replaces the original data.
|
||||||
// to the existing data. limit defines the maximum allowed size of the created array.
|
const replacer = <T>(update: T) => update;
|
||||||
const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit);
|
|
||||||
|
|
||||||
// replacer is a state update generalization function, which replaces the original data.
|
// appender is a state updater function, which appends the update data to the
|
||||||
const replacer = <T>(prev: T, update: T) => update;
|
// existing data. limit defines the maximum allowed size of the created array,
|
||||||
|
// mapper maps the update data.
|
||||||
|
const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, prev: Array<T>) => [
|
||||||
|
...prev,
|
||||||
|
...update.map(sample => mapper(sample)),
|
||||||
|
].slice(-limit);
|
||||||
|
|
||||||
// defaultContent is the initial value of the state content.
|
// defaultContent is the initial value of the state content.
|
||||||
const defaultContent: Content = {
|
const defaultContent: Content = {
|
||||||
|
|
@ -79,8 +82,11 @@ const defaultContent: Content = {
|
||||||
commit: null,
|
commit: null,
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
memory: [],
|
activeMemory: [],
|
||||||
traffic: [],
|
virtualMemory: [],
|
||||||
|
ingress: [],
|
||||||
|
egress: [],
|
||||||
|
cpu: [],
|
||||||
},
|
},
|
||||||
chain: {},
|
chain: {},
|
||||||
txpool: {},
|
txpool: {},
|
||||||
|
|
@ -91,16 +97,20 @@ const defaultContent: Content = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// updaters contains the state update generalization functions for each path of the state.
|
// updaters contains the state updater functions for each path of the state.
|
||||||
// TODO (kurkomisi): Define a tricky type which embraces the content and the handlers.
|
//
|
||||||
|
// TODO (kurkomisi): Define a tricky type which embraces the content and the updaters.
|
||||||
const updaters = {
|
const updaters = {
|
||||||
general: {
|
general: {
|
||||||
version: replacer,
|
version: replacer,
|
||||||
commit: replacer,
|
commit: replacer,
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
memory: appender(200),
|
activeMemory: appender(200),
|
||||||
traffic: appender(200),
|
virtualMemory: appender(200),
|
||||||
|
ingress: appender(200),
|
||||||
|
egress: appender(200),
|
||||||
|
cpu: appender(200),
|
||||||
},
|
},
|
||||||
chain: null,
|
chain: null,
|
||||||
txpool: null,
|
txpool: null,
|
||||||
|
|
@ -111,28 +121,34 @@ const updaters = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// styles returns the styles for the Dashboard component.
|
// styles contains the constant styles of the component.
|
||||||
const styles = theme => ({
|
const styles = {
|
||||||
dashboard: {
|
dashboard: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: theme.palette.background.default,
|
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// themeStyles returns the styles generated from the theme for the component.
|
||||||
|
const themeStyles: Object = (theme: Object) => ({
|
||||||
|
dashboard: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
classes: Object,
|
classes: Object, // injected by withStyles()
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
active: string, // active menu
|
active: string, // active menu
|
||||||
sideBar: boolean, // true if the sidebar is opened
|
sideBar: boolean, // true if the sidebar is opened
|
||||||
content: Content, // the visualized data
|
content: Content, // the visualized data
|
||||||
shouldUpdate: Object // labels for the components, which need to rerender based on the incoming message
|
shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dashboard is the main component, which renders the whole page, makes connection with the server and
|
// Dashboard is the main component, which renders the whole page, makes connection with the server and
|
||||||
|
|
@ -176,8 +192,8 @@ class Dashboard extends Component<Props, State> {
|
||||||
// update updates the content corresponding to the incoming message.
|
// update updates the content corresponding to the incoming message.
|
||||||
update = (msg: $Shape<Content>) => {
|
update = (msg: $Shape<Content>) => {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
content: deepUpdate(prevState.content, msg, updaters),
|
content: deepUpdate(updaters, msg, prevState.content),
|
||||||
shouldUpdate: shouldUpdate(msg, updaters),
|
shouldUpdate: shouldUpdate(updaters, msg),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -186,25 +202,17 @@ class Dashboard extends Component<Props, State> {
|
||||||
this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
|
this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// openSideBar opens the sidebar.
|
// switchSideBar opens or closes the sidebar's state.
|
||||||
openSideBar = () => {
|
switchSideBar = () => {
|
||||||
this.setState({sideBar: true});
|
this.setState(prevState => ({sideBar: !prevState.sideBar}));
|
||||||
};
|
|
||||||
|
|
||||||
// closeSideBar closes the sidebar.
|
|
||||||
closeSideBar = () => {
|
|
||||||
this.setState({sideBar: false});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {classes} = this.props; // The classes property is injected by withStyles().
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.dashboard}>
|
<div className={this.props.classes.dashboard} style={styles.dashboard}>
|
||||||
<Header
|
<Header
|
||||||
opened={this.state.sideBar}
|
opened={this.state.sideBar}
|
||||||
openSideBar={this.openSideBar}
|
switchSideBar={this.switchSideBar}
|
||||||
closeSideBar={this.closeSideBar}
|
|
||||||
/>
|
/>
|
||||||
<Body
|
<Body
|
||||||
opened={this.state.sideBar}
|
opened={this.state.sideBar}
|
||||||
|
|
@ -213,16 +221,9 @@ class Dashboard extends Component<Props, State> {
|
||||||
content={this.state.content}
|
content={this.state.content}
|
||||||
shouldUpdate={this.state.shouldUpdate}
|
shouldUpdate={this.state.shouldUpdate}
|
||||||
/>
|
/>
|
||||||
<Footer
|
|
||||||
opened={this.state.sideBar}
|
|
||||||
openSideBar={this.openSideBar}
|
|
||||||
closeSideBar={this.closeSideBar}
|
|
||||||
general={this.state.content.general}
|
|
||||||
shouldUpdate={this.state.shouldUpdate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Dashboard);
|
export default withStyles(themeStyles)(Dashboard);
|
||||||
|
|
|
||||||
|
|
@ -19,45 +19,117 @@
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
import withStyles from 'material-ui/styles/withStyles';
|
import withStyles from 'material-ui/styles/withStyles';
|
||||||
import AppBar from 'material-ui/AppBar';
|
|
||||||
import Toolbar from 'material-ui/Toolbar';
|
|
||||||
import Typography from 'material-ui/Typography';
|
import Typography from 'material-ui/Typography';
|
||||||
|
import Grid from 'material-ui/Grid';
|
||||||
|
import {ResponsiveContainer, AreaChart, Area, Tooltip} from 'recharts';
|
||||||
|
|
||||||
import type {General} from '../types/content';
|
import ChartRow from './ChartRow';
|
||||||
|
import CustomTooltip, {bytePlotter, valuePlotter, multiplier} from './CustomTooltip';
|
||||||
|
import {styles as commonStyles} from '../common';
|
||||||
|
import type {Content} from '../types/content';
|
||||||
|
|
||||||
// styles contains styles for the Header component.
|
// styles contains the constant styles of the component.
|
||||||
const styles = theme => ({
|
const styles = {
|
||||||
|
footer: {
|
||||||
|
maxWidth: '100%',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
chartRowWrapper: {
|
||||||
|
height: '100%',
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
doubleChartWrapper: {
|
||||||
|
height: '100%',
|
||||||
|
width: '99%',
|
||||||
|
paddingTop: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// themeStyles returns the styles generated from the theme for the component.
|
||||||
|
const themeStyles: Object = (theme: Object) => ({
|
||||||
footer: {
|
footer: {
|
||||||
backgroundColor: theme.palette.background.appBar,
|
backgroundColor: theme.palette.background.appBar,
|
||||||
color: theme.palette.getContrastText(theme.palette.background.appBar),
|
color: theme.palette.getContrastText(theme.palette.background.appBar),
|
||||||
zIndex: theme.zIndex.appBar,
|
zIndex: theme.zIndex.appBar,
|
||||||
},
|
height: theme.spacing.unit * 10,
|
||||||
toolbar: {
|
|
||||||
paddingLeft: theme.spacing.unit,
|
|
||||||
paddingRight: theme.spacing.unit,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
light: {
|
|
||||||
color: 'rgba(255, 255, 255, 0.54)',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
general: General,
|
classes: Object, // injected by withStyles()
|
||||||
classes: Object,
|
theme: Object,
|
||||||
|
content: Content,
|
||||||
|
shouldUpdate: Object,
|
||||||
};
|
};
|
||||||
// TODO (kurkomisi): If the structure is appropriate, make an abstraction of the common parts with the Header.
|
|
||||||
// Footer renders the header of the dashboard.
|
// Footer renders the footer of the dashboard.
|
||||||
class Footer extends Component<Props> {
|
class Footer extends Component<Props> {
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
return typeof nextProps.shouldUpdate.logs !== 'undefined';
|
return typeof nextProps.shouldUpdate.home !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
info = (about: string, data: string) => (
|
// info renders a label with the given values.
|
||||||
<Typography type="caption" color="inherit">
|
info = (about: string, value: ?string) => (value ? (
|
||||||
<span className={this.props.classes.light}>{about}</span> {data}
|
<Typography type='caption' color='inherit'>
|
||||||
|
<span style={commonStyles.light}>{about}</span> {value}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
) : null);
|
||||||
|
|
||||||
|
// doubleChart renders a pair of charts separated by the baseline.
|
||||||
|
doubleChart = (syncId, topChart, bottomChart) => {
|
||||||
|
const topKey = 'topKey';
|
||||||
|
const bottomKey = 'bottomKey';
|
||||||
|
const topDefault = topChart.default ? topChart.default : 0;
|
||||||
|
const bottomDefault = bottomChart.default ? bottomChart.default : 0;
|
||||||
|
const topTooltip = topChart.tooltip ? (
|
||||||
|
<Tooltip cursor={false} content={<CustomTooltip tooltip={topChart.tooltip} />} />
|
||||||
|
) : null;
|
||||||
|
const bottomTooltip = bottomChart.tooltip ? (
|
||||||
|
<Tooltip cursor={false} content={<CustomTooltip tooltip={bottomChart.tooltip} />} />
|
||||||
|
) : null;
|
||||||
|
const topColor = '#8884d8';
|
||||||
|
const bottomColor = '#82ca9d';
|
||||||
|
|
||||||
|
// Put the samples of the two charts into the same array in order to avoid problems
|
||||||
|
// at the synchronized area charts. If one of the two arrays doesn't have value at
|
||||||
|
// a given position, give it a 0 default value.
|
||||||
|
let data = [...topChart.data.map(({value}) => {
|
||||||
|
const d = {};
|
||||||
|
d[topKey] = value || topDefault;
|
||||||
|
return d;
|
||||||
|
})];
|
||||||
|
for (let i = 0; i < data.length && i < bottomChart.data.length; i++) {
|
||||||
|
// The value needs to be negative in order to plot it upside down.
|
||||||
|
const d = bottomChart.data[i];
|
||||||
|
data[i][bottomKey] = d && d.value ? -d.value : bottomDefault;
|
||||||
|
}
|
||||||
|
data = [...data, ...bottomChart.data.slice(data.length).map(({value}) => {
|
||||||
|
const d = {};
|
||||||
|
d[topKey] = topDefault;
|
||||||
|
d[bottomKey] = -value || bottomDefault;
|
||||||
|
return d;
|
||||||
|
})];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.doubleChartWrapper}>
|
||||||
|
<ResponsiveContainer width='100%' height='50%'>
|
||||||
|
<AreaChart data={data} syncId={syncId} >
|
||||||
|
{topTooltip}
|
||||||
|
<Area type='monotone' dataKey={topKey} stroke={topColor} fill={topColor} />
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<div style={{marginTop: -10, width: '100%', height: '50%'}}>
|
||||||
|
<ResponsiveContainer width='100%' height='100%'>
|
||||||
|
<AreaChart data={data} syncId={syncId} >
|
||||||
|
{bottomTooltip}
|
||||||
|
<Area type='monotone' dataKey={bottomKey} stroke={bottomColor} fill={bottomColor} />
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {classes, general} = this.props; // The classes property is injected by withStyles().
|
const {classes, general} = this.props; // The classes property is injected by withStyles().
|
||||||
|
|
@ -65,16 +137,33 @@ class Footer extends Component<Props> {
|
||||||
const commit = general.commit ? this.info('Commit', general.commit.substring(0, 7)) : null;
|
const commit = general.commit ? this.info('Commit', general.commit.substring(0, 7)) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="static" className={classes.footer}>
|
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
|
||||||
<Toolbar className={classes.toolbar}>
|
<Grid item xs style={styles.chartRowWrapper}>
|
||||||
<div>
|
<ChartRow>
|
||||||
{geth}
|
{this.doubleChart(
|
||||||
{commit}
|
'all',
|
||||||
</div>
|
{data: home.cpu, tooltip: valuePlotter('CPU')},
|
||||||
</Toolbar>
|
{data: home.cpu, tooltip: valuePlotter('CPU', multiplier(-1))},
|
||||||
</AppBar>
|
)}
|
||||||
|
{this.doubleChart(
|
||||||
|
'all',
|
||||||
|
{data: home.activeMemory, tooltip: bytePlotter('Active')},
|
||||||
|
{data: home.virtualMemory, tooltip: bytePlotter('Virtual', multiplier(-1))},
|
||||||
|
)}
|
||||||
|
{this.doubleChart(
|
||||||
|
'all',
|
||||||
|
{data: home.ingress, tooltip: bytePlotter('Download')},
|
||||||
|
{data: home.egress, tooltip: bytePlotter('Upload', multiplier(-1))},
|
||||||
|
)}
|
||||||
|
</ChartRow>
|
||||||
|
</Grid>
|
||||||
|
<Grid item >
|
||||||
|
{this.info('Geth', general.version)}
|
||||||
|
{this.info('Commit', general.commit ? general.commit.substring(0, 7) : null)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Footer);
|
export default withStyles(themeStyles)(Footer);
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,22 @@ import IconButton from 'material-ui/IconButton';
|
||||||
import Typography from 'material-ui/Typography';
|
import Typography from 'material-ui/Typography';
|
||||||
import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
|
import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
|
||||||
|
|
||||||
import {DURATION} from './Common';
|
import {DURATION} from '../common';
|
||||||
|
|
||||||
// arrowDefault is the default style of the arrow button.
|
// styles contains the constant styles of the component.
|
||||||
const arrowDefault = {
|
const styles = {
|
||||||
|
arrow: {
|
||||||
|
default: {
|
||||||
transition: `transform ${DURATION}ms`,
|
transition: `transform ${DURATION}ms`,
|
||||||
};
|
},
|
||||||
// arrowTransition is the additional style of the arrow button corresponding to the transition's state.
|
transition: {
|
||||||
const arrowTransition = {
|
|
||||||
entered: {transform: 'rotate(180deg)'},
|
entered: {transform: 'rotate(180deg)'},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// Styles for the Header component.
|
|
||||||
const styles = theme => ({
|
// themeStyles returns the styles generated from the theme for the component.
|
||||||
|
const themeStyles = (theme: Object) => ({
|
||||||
header: {
|
header: {
|
||||||
backgroundColor: theme.palette.background.appBar,
|
backgroundColor: theme.palette.background.appBar,
|
||||||
color: theme.palette.getContrastText(theme.palette.background.appBar),
|
color: theme.palette.getContrastText(theme.palette.background.appBar),
|
||||||
|
|
@ -47,53 +51,45 @@ const styles = theme => ({
|
||||||
paddingLeft: theme.spacing.unit,
|
paddingLeft: theme.spacing.unit,
|
||||||
paddingRight: theme.spacing.unit,
|
paddingRight: theme.spacing.unit,
|
||||||
},
|
},
|
||||||
mainText: {
|
title: {
|
||||||
paddingLeft: theme.spacing.unit,
|
paddingLeft: theme.spacing.unit,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
classes: Object,
|
classes: Object, // injected by withStyles()
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
openSideBar: () => {},
|
switchSideBar: () => void,
|
||||||
closeSideBar: () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Header renders the header of the dashboard.
|
// Header renders the header of the dashboard.
|
||||||
class Header extends Component<Props> {
|
class Header extends Component<Props> {
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
return nextProps.opened !== this.props.opened;
|
return nextProps.opened !== this.props.opened;
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeSideBar opens or closes the sidebar corresponding to the previous state.
|
// arrow renders a button, which changes the sidebar's state.
|
||||||
changeSideBar = () => {
|
arrow = (transitionState: string) => (
|
||||||
if (this.props.opened) {
|
<IconButton onClick={this.props.switchSideBar}>
|
||||||
this.props.closeSideBar();
|
|
||||||
} else {
|
|
||||||
this.props.openSideBar();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// arrowButton is connected to the sidebar; changes its state.
|
|
||||||
arrowButton = (transitionState: string) => (
|
|
||||||
<IconButton onClick={this.changeSideBar}>
|
|
||||||
<ChevronLeftIcon
|
<ChevronLeftIcon
|
||||||
style={{
|
style={{
|
||||||
...arrowDefault,
|
...styles.arrow.default,
|
||||||
...arrowTransition[transitionState],
|
...styles.arrow.transition[transitionState],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {classes, opened} = this.props; // The classes property is injected by withStyles().
|
const {classes, opened} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="static" className={classes.header}>
|
<AppBar position='static' className={classes.header}>
|
||||||
<Toolbar className={classes.toolbar}>
|
<Toolbar className={classes.toolbar}>
|
||||||
<Transition mountOnEnter in={opened} timeout={{enter: DURATION}}>
|
<Transition mountOnEnter in={opened} timeout={{enter: DURATION}}>
|
||||||
{this.arrowButton}
|
{this.arrow}
|
||||||
</Transition>
|
</Transition>
|
||||||
<Typography type="title" color="inherit" noWrap className={classes.mainText}>
|
<Typography type='title' color='inherit' noWrap className={classes.title}>
|
||||||
Go Ethereum Dashboard
|
Go Ethereum Dashboard
|
||||||
</Typography>
|
</Typography>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
@ -102,4 +98,4 @@ class Header extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Header);
|
export default withStyles(themeStyles)(Header);
|
||||||
|
|
|
||||||
|
|
@ -19,55 +19,50 @@
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
import withTheme from 'material-ui/styles/withTheme';
|
import withTheme from 'material-ui/styles/withTheme';
|
||||||
import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line} from 'recharts';
|
import {LineChart, AreaChart, Area, Line} from 'recharts';
|
||||||
|
|
||||||
import ChartGrid from './ChartGrid';
|
import ChartGrid from './ChartGrid';
|
||||||
import type {ChartEntry} from '../types/content';
|
import type {ChartEntry} from '../types/content';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
theme: Object,
|
theme: Object,
|
||||||
memory: Array<ChartEntry>,
|
activeMemory: Array<ChartEntry>,
|
||||||
traffic: Array<ChartEntry>,
|
ingress: Array<ChartEntry>,
|
||||||
|
egress: Array<ChartEntry>,
|
||||||
|
cpu: Array<ChartEntry>,
|
||||||
shouldUpdate: Object,
|
shouldUpdate: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Home renders the home content.
|
// Home renders the home content.
|
||||||
class Home extends Component<Props> {
|
class Home extends Component<Props> {
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
const {theme} = props; // The theme property is injected by withTheme().
|
|
||||||
this.memoryColor = theme.palette.primary[300];
|
|
||||||
this.trafficColor = theme.palette.secondary[300];
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
return typeof nextProps.shouldUpdate.home !== 'undefined';
|
return typeof nextProps.shouldUpdate.home !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryColor: Object;
|
|
||||||
trafficColor: Object;
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let {memory, traffic} = this.props;
|
let {
|
||||||
memory = memory.map(({value}) => (value || 0));
|
activeMemory, ingress, egress, cpu
|
||||||
traffic = traffic.map(({value}) => (value || 0));
|
} = this.props;
|
||||||
|
activeMemory = activeMemory.map(({value}) => (value || 0));
|
||||||
|
cpu = cpu.map(({value}) => (value || 0));
|
||||||
|
ingress = ingress.map(({value}) => (value || 0));
|
||||||
|
egress = egress.map(({value}) => (value || 0));
|
||||||
|
|
||||||
|
const color = '#8884d8';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartGrid spacing={24}>
|
<ChartGrid spacing={24}>
|
||||||
<AreaChart xs={6} height={300} values={memory}>
|
<AreaChart xs={6} height={300} values={activeMemory}>
|
||||||
<YAxis />
|
<Area type='monotone' dataKey='value' stroke={color} fill={color} />
|
||||||
<Area type="monotone" dataKey="value" stroke={this.memoryColor} fill={this.memoryColor} />
|
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
<LineChart xs={6} height={300} values={traffic}>
|
<LineChart xs={6} height={300} values={ingress}>
|
||||||
<Line type="monotone" dataKey="value" stroke={this.trafficColor} dot={false} />
|
<Line type='monotone' dataKey='value' stroke={color} dot={false} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
<LineChart xs={6} height={300} values={memory}>
|
<LineChart xs={6} height={300} values={cpu}>
|
||||||
<YAxis />
|
<Line type='monotone' dataKey='value' stroke={color} dot={false} />
|
||||||
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
|
|
||||||
<Line type="monotone" dataKey="value" stroke={this.memoryColor} dot={false} />
|
|
||||||
</LineChart>
|
</LineChart>
|
||||||
<AreaChart xs={6} height={300} values={traffic}>
|
<AreaChart xs={6} height={300} values={egress}>
|
||||||
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
|
<Area type='monotone' dataKey='value' stroke={color} fill={color} />
|
||||||
<Area type="monotone" dataKey="value" stroke={this.trafficColor} fill={this.trafficColor} />
|
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartGrid>
|
</ChartGrid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,35 +21,58 @@ import React, {Component} from 'react';
|
||||||
import withStyles from 'material-ui/styles/withStyles';
|
import withStyles from 'material-ui/styles/withStyles';
|
||||||
|
|
||||||
import Home from './Home';
|
import Home from './Home';
|
||||||
import {MENU} from './Common';
|
import {MENU} from '../common';
|
||||||
|
import Footer from './Footer';
|
||||||
import type {Content} from '../types/content';
|
import type {Content} from '../types/content';
|
||||||
|
|
||||||
// Styles for the Content component.
|
// styles contains the constant styles of the component.
|
||||||
const styles = theme => ({
|
const styles = {
|
||||||
|
wrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
flexGrow: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.palette.background.default,
|
|
||||||
padding: theme.spacing.unit * 3,
|
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// themeStyles returns the styles generated from the theme for the component.
|
||||||
|
const themeStyles = theme => ({
|
||||||
|
content: {
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
padding: theme.spacing.unit * 3,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
active: string,
|
active: string,
|
||||||
content: Content,
|
content: Content,
|
||||||
shouldUpdate: Object,
|
shouldUpdate: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main renders the chosen content.
|
// Main renders the chosen content.
|
||||||
class Main extends Component<Props> {
|
class Main extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
classes, active, content, shouldUpdate,
|
classes, active, content, shouldUpdate,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const {home} = content;
|
||||||
|
|
||||||
let children = null;
|
let children = null;
|
||||||
switch (active) {
|
switch (active) {
|
||||||
case MENU.get('home').id:
|
case MENU.get('home').id:
|
||||||
children = <Home memory={content.home.memory} traffic={content.home.traffic} shouldUpdate={shouldUpdate} />;
|
children = (
|
||||||
|
<Home
|
||||||
|
activeMemory={home.activeMemory}
|
||||||
|
ingress={home.ingress}
|
||||||
|
egress={home.egress}
|
||||||
|
cpu={home.cpu}
|
||||||
|
shouldUpdate={shouldUpdate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case MENU.get('chain').id:
|
case MENU.get('chain').id:
|
||||||
case MENU.get('txpool').id:
|
case MENU.get('txpool').id:
|
||||||
|
|
@ -61,8 +84,16 @@ class Main extends Component<Props> {
|
||||||
children = <div>{content.logs.log.map((log, index) => <div key={index}>{log}</div>)}</div>;
|
children = <div>{content.logs.log.map((log, index) => <div key={index}>{log}</div>)}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={classes.content}>{children}</div>;
|
return (
|
||||||
|
<div style={styles.wrapper}>
|
||||||
|
<div className={classes.content} style={styles.content}>{children}</div>
|
||||||
|
<Footer
|
||||||
|
content={content}
|
||||||
|
shouldUpdate={shouldUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Main);
|
export default withStyles(themeStyles)(Main);
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,22 @@ import Icon from 'material-ui/Icon';
|
||||||
import Transition from 'react-transition-group/Transition';
|
import Transition from 'react-transition-group/Transition';
|
||||||
import {Icon as FontAwesome} from 'react-fa';
|
import {Icon as FontAwesome} from 'react-fa';
|
||||||
|
|
||||||
import {MENU, DURATION} from './Common';
|
import {MENU, DURATION} from '../common';
|
||||||
|
|
||||||
// menuDefault is the default style of the menu.
|
// styles contains the constant styles of the component.
|
||||||
const menuDefault = {
|
const styles = {
|
||||||
|
menu: {
|
||||||
|
default: {
|
||||||
transition: `margin-left ${DURATION}ms`,
|
transition: `margin-left ${DURATION}ms`,
|
||||||
};
|
},
|
||||||
// menuTransition is the additional style of the menu corresponding to the transition's state.
|
transition: {
|
||||||
const menuTransition = {
|
|
||||||
entered: {marginLeft: -200},
|
entered: {marginLeft: -200},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// Styles for the SideBar component.
|
|
||||||
const styles = theme => ({
|
// themeStyles returns the styles generated from the theme for the component.
|
||||||
|
const themeStyles = theme => ({
|
||||||
list: {
|
list: {
|
||||||
background: theme.palette.background.appBar,
|
background: theme.palette.background.appBar,
|
||||||
},
|
},
|
||||||
|
|
@ -46,38 +50,32 @@ const styles = theme => ({
|
||||||
fontSize: theme.spacing.unit * 3,
|
fontSize: theme.spacing.unit * 3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
classes: Object,
|
classes: Object, // injected by withStyles()
|
||||||
opened: boolean,
|
opened: boolean,
|
||||||
changeContent: () => {},
|
changeContent: string => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
// SideBar renders the sidebar of the dashboard.
|
// SideBar renders the sidebar of the dashboard.
|
||||||
class SideBar extends Component<Props> {
|
class SideBar extends Component<Props> {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
// clickOn contains onClick event functions for the menu items.
|
|
||||||
// Instantiate only once, and reuse the existing functions to prevent the creation of
|
|
||||||
// new function instances every time the render method is triggered.
|
|
||||||
this.clickOn = {};
|
|
||||||
MENU.forEach((menu) => {
|
|
||||||
this.clickOn[menu.id] = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
props.changeContent(menu.id);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
return nextProps.opened !== this.props.opened;
|
return nextProps.opened !== this.props.opened;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clickOn returns a click event handler function for the given menu item.
|
||||||
|
clickOn = menu => (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.props.changeContent(menu);
|
||||||
|
};
|
||||||
|
|
||||||
|
// menuItems returns the menu items corresponding to the sidebar state.
|
||||||
menuItems = (transitionState) => {
|
menuItems = (transitionState) => {
|
||||||
const {classes} = this.props;
|
const {classes} = this.props;
|
||||||
const children = [];
|
const children = [];
|
||||||
MENU.forEach((menu) => {
|
MENU.forEach((menu) => {
|
||||||
children.push(
|
children.push((
|
||||||
<ListItem button key={menu.id} onClick={this.clickOn[menu.id]} className={classes.listItem}>
|
<ListItem button key={menu.id} onClick={this.clickOn(menu.id)} className={classes.listItem}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Icon className={classes.icon}>
|
<Icon className={classes.icon}>
|
||||||
<FontAwesome name={menu.icon} />
|
<FontAwesome name={menu.icon} />
|
||||||
|
|
@ -86,29 +84,25 @@ class SideBar extends Component<Props> {
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={menu.title}
|
primary={menu.title}
|
||||||
style={{
|
style={{
|
||||||
...menuDefault,
|
...styles.menu.default,
|
||||||
...menuTransition[transitionState],
|
...styles.menu.transition[transitionState],
|
||||||
padding: 0,
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>,
|
</ListItem>
|
||||||
);
|
));
|
||||||
});
|
});
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
// menu renders the list of the menu items.
|
// menu renders the list of the menu items.
|
||||||
menu = (transitionState) => {
|
menu = (transitionState: Object) => (
|
||||||
const {classes} = this.props; // The classes property is injected by withStyles().
|
<div className={this.props.classes.list}>
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.list}>
|
|
||||||
<List>
|
<List>
|
||||||
{this.menuItems(transitionState)}
|
{this.menuItems(transitionState)}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -119,4 +113,4 @@ class SideBar extends Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(SideBar);
|
export default withStyles(themeStyles)(SideBar);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import createMuiTheme from 'material-ui/styles/createMuiTheme';
|
||||||
|
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme: Object = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: 'dark',
|
type: 'dark',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
566
dashboard/assets/package-lock.json
generated
566
dashboard/assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.0",
|
||||||
"babel-eslint": "^8.1.2",
|
"babel-eslint": "^8.2.1",
|
||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.2",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
"babel-preset-stage-0": "^6.24.1",
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"css-loader": "^0.28.8",
|
"css-loader": "^0.28.9",
|
||||||
"eslint": "^4.15.0",
|
"eslint": "^4.16.0",
|
||||||
"eslint-config-airbnb": "^16.1.0",
|
"eslint-config-airbnb": "^16.1.0",
|
||||||
"eslint-loader": "^1.9.0",
|
"eslint-loader": "^1.9.0",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"eslint-plugin-import": "^2.8.0",
|
||||||
|
|
@ -24,16 +24,15 @@
|
||||||
"flow-bin": "^0.63.1",
|
"flow-bin": "^0.63.1",
|
||||||
"flow-bin-loader": "^1.0.2",
|
"flow-bin-loader": "^1.0.2",
|
||||||
"flow-typed": "^2.2.3",
|
"flow-typed": "^2.2.3",
|
||||||
"material-ui": "^1.0.0-beta.24",
|
"material-ui": "^1.0.0-beta.30",
|
||||||
"material-ui-icons": "^1.0.0-beta.17",
|
"material-ui-icons": "^1.0.0-beta.17",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"react": "^16.2.0",
|
"react": "^16.2.0",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.2.0",
|
||||||
"react-fa": "^5.0.0",
|
"react-fa": "^5.0.0",
|
||||||
"react-transition-group": "^2.2.1",
|
"react-transition-group": "^2.2.1",
|
||||||
"recharts": "^1.0.0-beta.7",
|
"recharts": "^1.0.0-beta.9",
|
||||||
"style-loader": "^0.19.1",
|
"style-loader": "^0.19.1",
|
||||||
"typeface-roboto": "^0.0.50",
|
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"url-loader": "^0.6.2",
|
"url-loader": "^0.6.2",
|
||||||
"webpack": "^3.10.0"
|
"webpack": "^3.10.0"
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,11 @@ export type General = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Home = {
|
export type Home = {
|
||||||
memory: ChartEntries,
|
activeMemory: ChartEntries,
|
||||||
traffic: ChartEntries,
|
virtualMemory: ChartEntries,
|
||||||
|
ingress: ChartEntries,
|
||||||
|
egress: ChartEntries,
|
||||||
|
cpu: ChartEntries,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChartEntries = Array<ChartEntry>;
|
export type ChartEntries = Array<ChartEntry>;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import "time"
|
||||||
var DefaultConfig = Config{
|
var DefaultConfig = Config{
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Refresh: 3 * time.Second,
|
Refresh: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains the configuration parameters of the dashboard.
|
// Config contains the configuration parameters of the dashboard.
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/elastic/gosigar"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
|
@ -42,8 +44,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
memorySampleLimit = 200 // Maximum number of memory data samples
|
activeMemorySampleLimit = 200 // Maximum number of active memory data samples
|
||||||
trafficSampleLimit = 200 // Maximum number of traffic data samples
|
virtualMemorySampleLimit = 200 // Maximum number of virtual memory data samples
|
||||||
|
ingressSampleLimit = 200 // Maximum number of ingress data samples
|
||||||
|
egressSampleLimit = 200 // Maximum number of egress data samples
|
||||||
|
cpuSampleLimit = 200 // Maximum number of cpu data samples
|
||||||
)
|
)
|
||||||
|
|
||||||
var nextID uint32 // Next connection id
|
var nextID uint32 // Next connection id
|
||||||
|
|
@ -76,8 +81,10 @@ func New(config *Config, commit string) (*Dashboard, error) {
|
||||||
config: config,
|
config: config,
|
||||||
quit: make(chan chan error),
|
quit: make(chan chan error),
|
||||||
charts: &HomeMessage{
|
charts: &HomeMessage{
|
||||||
Memory: ChartEntries{},
|
ActiveMemory: ChartEntries{},
|
||||||
Traffic: ChartEntries{},
|
VirtualMemory: ChartEntries{},
|
||||||
|
Ingress: ChartEntries{},
|
||||||
|
Egress: ChartEntries{},
|
||||||
},
|
},
|
||||||
commit: commit,
|
commit: commit,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -215,8 +222,11 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
Commit: db.commit,
|
Commit: db.commit,
|
||||||
},
|
},
|
||||||
Home: &HomeMessage{
|
Home: &HomeMessage{
|
||||||
Memory: db.charts.Memory,
|
ActiveMemory: db.charts.ActiveMemory,
|
||||||
Traffic: db.charts.Traffic,
|
VirtualMemory: db.charts.VirtualMemory,
|
||||||
|
Ingress: db.charts.Ingress,
|
||||||
|
Egress: db.charts.Egress,
|
||||||
|
CPU: db.charts.CPU,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Start tracking the connection and drop at connection loss.
|
// Start tracking the connection and drop at connection loss.
|
||||||
|
|
@ -242,38 +252,86 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
|
||||||
func (db *Dashboard) collectData() {
|
func (db *Dashboard) collectData() {
|
||||||
defer db.wg.Done()
|
defer db.wg.Done()
|
||||||
|
|
||||||
|
prevIn := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
||||||
|
prevOut := metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
||||||
|
cpuUsage := gosigar.Cpu{}
|
||||||
|
cpuUsage.Get()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case errc := <-db.quit:
|
case errc := <-db.quit:
|
||||||
errc <- nil
|
errc <- nil
|
||||||
return
|
return
|
||||||
case <-time.After(db.config.Refresh):
|
case <-time.After(db.config.Refresh):
|
||||||
inboundTraffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
|
var mem runtime.MemStats
|
||||||
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
|
runtime.ReadMemStats(&mem)
|
||||||
|
|
||||||
|
curIn := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
|
||||||
|
inboundTraffic := float64(curIn - prevIn)
|
||||||
|
prevIn = curIn
|
||||||
|
|
||||||
|
curOut := metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
|
||||||
|
outboundTraffic := float64(curOut - prevOut)
|
||||||
|
prevOut = curOut
|
||||||
|
|
||||||
|
prevCPUUsage := cpuUsage
|
||||||
|
cpuUsage.Get()
|
||||||
|
deltaCPU := cpuUsage.Delta(prevCPUUsage)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
memory := &ChartEntry{
|
activeMemory := &ChartEntry{
|
||||||
Time: now,
|
Time: now,
|
||||||
Value: memoryInUse,
|
Value: float64(mem.Alloc),
|
||||||
}
|
}
|
||||||
traffic := &ChartEntry{
|
virtualMemory := &ChartEntry{
|
||||||
|
Time: now,
|
||||||
|
Value: float64(mem.Sys),
|
||||||
|
}
|
||||||
|
ingress := &ChartEntry{
|
||||||
Time: now,
|
Time: now,
|
||||||
Value: inboundTraffic,
|
Value: inboundTraffic,
|
||||||
}
|
}
|
||||||
|
egress := &ChartEntry{
|
||||||
|
Time: now,
|
||||||
|
Value: outboundTraffic,
|
||||||
|
}
|
||||||
|
CPU := &ChartEntry{
|
||||||
|
Time: now,
|
||||||
|
Value: float64(deltaCPU.User),
|
||||||
|
}
|
||||||
first := 0
|
first := 0
|
||||||
if len(db.charts.Memory) == memorySampleLimit {
|
if len(db.charts.ActiveMemory) == activeMemorySampleLimit {
|
||||||
first = 1
|
first = 1
|
||||||
}
|
}
|
||||||
db.charts.Memory = append(db.charts.Memory[first:], memory)
|
db.charts.ActiveMemory = append(db.charts.ActiveMemory[first:], activeMemory)
|
||||||
first = 0
|
first = 0
|
||||||
if len(db.charts.Traffic) == trafficSampleLimit {
|
if len(db.charts.VirtualMemory) == virtualMemorySampleLimit {
|
||||||
first = 1
|
first = 1
|
||||||
}
|
}
|
||||||
db.charts.Traffic = append(db.charts.Traffic[first:], traffic)
|
db.charts.VirtualMemory = append(db.charts.VirtualMemory[first:], virtualMemory)
|
||||||
|
first = 0
|
||||||
|
if len(db.charts.Ingress) == ingressSampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.Ingress = append(db.charts.Ingress[first:], ingress)
|
||||||
|
first = 0
|
||||||
|
if len(db.charts.Egress) == egressSampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.Egress = append(db.charts.Egress[first:], egress)
|
||||||
|
first = 0
|
||||||
|
if len(db.charts.CPU) == cpuSampleLimit {
|
||||||
|
first = 1
|
||||||
|
}
|
||||||
|
db.charts.CPU = append(db.charts.CPU[first:], CPU)
|
||||||
|
|
||||||
db.sendToAll(&Message{
|
db.sendToAll(&Message{
|
||||||
Home: &HomeMessage{
|
Home: &HomeMessage{
|
||||||
Memory: ChartEntries{memory},
|
ActiveMemory: ChartEntries{activeMemory},
|
||||||
Traffic: ChartEntries{traffic},
|
VirtualMemory: ChartEntries{virtualMemory},
|
||||||
|
Ingress: ChartEntries{ingress},
|
||||||
|
Egress: ChartEntries{egress},
|
||||||
|
CPU: ChartEntries{CPU},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,11 @@ type GeneralMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HomeMessage struct {
|
type HomeMessage struct {
|
||||||
Memory ChartEntries `json:"memory,omitempty"`
|
ActiveMemory ChartEntries `json:"activeMemory,omitempty"`
|
||||||
Traffic ChartEntries `json:"traffic,omitempty"`
|
VirtualMemory ChartEntries `json:"virtualMemory,omitempty"`
|
||||||
|
Ingress ChartEntries `json:"ingress,omitempty"`
|
||||||
|
Egress ChartEntries `json:"egress,omitempty"`
|
||||||
|
CPU ChartEntries `json:"cpu,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartEntries []*ChartEntry
|
type ChartEntries []*ChartEntry
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue