aboutsummaryrefslogtreecommitdiffstats
path: root/dashboard/assets/components
diff options
context:
space:
mode:
Diffstat (limited to 'dashboard/assets/components')
-rw-r--r--dashboard/assets/components/Body.jsx64
-rw-r--r--dashboard/assets/components/ChartGrid.jsx49
-rw-r--r--dashboard/assets/components/Common.jsx107
-rw-r--r--dashboard/assets/components/Dashboard.jsx320
-rw-r--r--dashboard/assets/components/Header.jsx132
-rw-r--r--dashboard/assets/components/Home.jsx107
-rw-r--r--dashboard/assets/components/Main.jsx123
-rw-r--r--dashboard/assets/components/SideBar.jsx170
8 files changed, 616 insertions, 456 deletions
diff --git a/dashboard/assets/components/Body.jsx b/dashboard/assets/components/Body.jsx
new file mode 100644
index 000000000..14e9ac358
--- /dev/null
+++ b/dashboard/assets/components/Body.jsx
@@ -0,0 +1,64 @@
+// @flow
+
+// Copyright 2017 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 withStyles from 'material-ui/styles/withStyles';
+
+import SideBar from './SideBar';
+import Main from './Main';
+import type {Content} from '../types/content';
+
+// Styles for the Body component.
+const styles = () => ({
+ body: {
+ display: 'flex',
+ width: '100%',
+ height: '100%',
+ },
+});
+export type Props = {
+ classes: Object,
+ opened: boolean,
+ changeContent: () => {},
+ active: string,
+ content: Content,
+ shouldUpdate: Object,
+};
+// Body renders the body of the dashboard.
+class Body extends Component<Props> {
+ render() {
+ const {classes} = this.props; // The classes property is injected by withStyles().
+
+ return (
+ <div className={classes.body}>
+ <SideBar
+ opened={this.props.opened}
+ changeContent={this.props.changeContent}
+ />
+ <Main
+ active={this.props.active}
+ content={this.props.content}
+ shouldUpdate={this.props.shouldUpdate}
+ />
+ </div>
+ );
+ }
+}
+
+export default withStyles(styles)(Body);
diff --git a/dashboard/assets/components/ChartGrid.jsx b/dashboard/assets/components/ChartGrid.jsx
new file mode 100644
index 000000000..45dde7499
--- /dev/null
+++ b/dashboard/assets/components/ChartGrid.jsx
@@ -0,0 +1,49 @@
+// @flow
+
+// Copyright 2017 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 {Node} from 'react';
+
+import Grid from 'material-ui/Grid';
+import {ResponsiveContainer} from 'recharts';
+
+export type Props = {
+ spacing: number,
+ children: Node,
+};
+// ChartGrid renders a grid container for responsive charts.
+// The children are Recharts components extended with the Material-UI's xs property.
+class ChartGrid extends Component<Props> {
+ render() {
+ return (
+ <Grid container spacing={this.props.spacing}>
+ {
+ React.Children.map(this.props.children, child => (
+ <Grid item xs={child.props.xs}>
+ <ResponsiveContainer width="100%" height={child.props.height}>
+ {React.cloneElement(child, {data: child.props.values.map(value => ({value}))})}
+ </ResponsiveContainer>
+ </Grid>
+ ))
+ }
+ </Grid>
+ );
+ }
+}
+
+export default ChartGrid;
diff --git a/dashboard/assets/components/Common.jsx b/dashboard/assets/components/Common.jsx
index 5129939c5..d8723830e 100644
--- a/dashboard/assets/components/Common.jsx
+++ b/dashboard/assets/components/Common.jsx
@@ -1,3 +1,5 @@
+// @flow
+
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -14,39 +16,78 @@
// 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/>.
-// isNullOrUndefined returns true if the given variable is null or undefined.
-export const isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
-
-export const LIMIT = {
- memory: 200, // Maximum number of memory data samples.
- traffic: 200, // Maximum number of traffic data samples.
- log: 200, // Maximum number of logs.
-};
+type ProvidedMenuProp = {|title: string, icon: string|};
+const menuSkeletons: Array<{|id: string, menu: ProvidedMenuProp|}> = [
+ {
+ id: 'home',
+ menu: {
+ title: 'Home',
+ icon: 'home',
+ },
+ }, {
+ id: 'chain',
+ menu: {
+ title: 'Chain',
+ icon: 'link',
+ },
+ }, {
+ id: 'txpool',
+ menu: {
+ title: 'TxPool',
+ icon: 'credit-card',
+ },
+ }, {
+ id: 'network',
+ menu: {
+ title: 'Network',
+ icon: 'globe',
+ },
+ }, {
+ id: 'system',
+ menu: {
+ title: 'System',
+ icon: 'tachometer',
+ },
+ }, {
+ id: 'logs',
+ menu: {
+ title: 'Logs',
+ icon: 'list',
+ },
+ },
+];
+export type MenuProp = {|...ProvidedMenuProp, id: string|};
// The sidebar menu and the main content are rendered based on these elements.
-export const TAGS = (() => {
- const T = {
- home: { title: "Home", },
- chain: { title: "Chain", },
- transactions: { title: "Transactions", },
- network: { title: "Network", },
- system: { title: "System", },
- logs: { title: "Logs", },
- };
- // Using the key is circumstantial in some cases, so it is better to insert it also as a value.
- // This way the mistyping is prevented.
- for(let key in T) {
- T[key]['id'] = key;
- }
- return T;
-})();
+// Using the id is circumstantial in some cases, so it is better to insert it also as a value.
+// This way the mistyping is prevented.
+export const MENU: Map<string, {...MenuProp}> = new Map(menuSkeletons.map(({id, menu}) => ([id, {id, ...menu}])));
+
+type ProvidedSampleProp = {|limit: number|};
+const sampleSkeletons: Array<{|id: string, sample: ProvidedSampleProp|}> = [
+ {
+ id: 'memory',
+ sample: {
+ limit: 200,
+ },
+ }, {
+ id: 'traffic',
+ sample: {
+ limit: 200,
+ },
+ }, {
+ id: 'logs',
+ sample: {
+ limit: 200,
+ },
+ },
+];
+export type SampleProp = {|...ProvidedSampleProp, id: string|};
+export const SAMPLE: Map<string, {...SampleProp}> = new Map(sampleSkeletons.map(({id, sample}) => ([id, {id, ...sample}])));
-export const DATA_KEYS = (() => {
- const DK = {};
- ["memory", "traffic", "logs"].map(key => {
- DK[key] = key;
- });
- return DK;
-})();
+export const DURATION = 200;
-// Temporary - taken from Material-UI
-export const DRAWER_WIDTH = 240;
+export const LENS: Map<string, string> = new Map([
+ 'content',
+ ...menuSkeletons.map(({id}) => id),
+ ...sampleSkeletons.map(({id}) => id),
+].map(lens => [lens, lens]));
diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx
index 740acf959..b60736d8c 100644
--- a/dashboard/assets/components/Dashboard.jsx
+++ b/dashboard/assets/components/Dashboard.jsx
@@ -1,3 +1,5 @@
+// @flow
+
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -15,155 +17,183 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withStyles} from 'material-ui/styles';
-import SideBar from './SideBar.jsx';
-import Header from './Header.jsx';
-import Main from "./Main.jsx";
-import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS,} from "./Common.jsx";
+import withStyles from 'material-ui/styles/withStyles';
+import {lensPath, view, set} from 'ramda';
+
+import Header from './Header';
+import Body from './Body';
+import {MENU, SAMPLE} from './Common';
+import type {Message, HomeMessage, LogsMessage, Chart} from '../types/message';
+import type {Content} from '../types/content';
-// Styles for the Dashboard component.
+// appender appends an array (A) to the end of another array (B) in the state.
+// lens is the path of B in the state, samples is A, and limit is the maximum size of the changed array.
+//
+// appender retrieves a function, which overrides the state's value at lens, and returns with the overridden state.
+const appender = (lens, samples, limit) => (state) => {
+ const newSamples = [
+ ...view(lens, state), // retrieves a specific value of the state at the given path (lens).
+ ...samples,
+ ];
+ // set is a function of ramda.js, which needs the path, the new value, the original state, and retrieves
+ // the altered state.
+ return set(
+ lens,
+ newSamples.slice(newSamples.length > limit ? newSamples.length - limit : 0),
+ state
+ );
+};
+// Lenses for specific data fields in the state, used for a clearer deep update.
+// NOTE: This solution will be changed very likely.
+const memoryLens = lensPath(['content', 'home', 'memory']);
+const trafficLens = lensPath(['content', 'home', 'traffic']);
+const logLens = lensPath(['content', 'logs', 'log']);
+// styles retrieves the styles for the Dashboard component.
const styles = theme => ({
- appFrame: {
- position: 'relative',
- display: 'flex',
- width: '100%',
- height: '100%',
- background: theme.palette.background.default,
- },
+ dashboard: {
+ display: 'flex',
+ flexFlow: 'column',
+ width: '100%',
+ height: '100%',
+ background: theme.palette.background.default,
+ zIndex: 1,
+ overflow: 'hidden',
+ },
});
-
-// Dashboard is the main component, which renders the whole page, makes connection with the server and listens for messages.
-// When there is an incoming message, updates the page's content correspondingly.
-class Dashboard extends Component {
- constructor(props) {
- super(props);
- this.state = {
- active: TAGS.home.id, // active menu
- sideBar: true, // true if the sidebar is opened
- memory: [],
- traffic: [],
- logs: [],
- shouldUpdate: {},
- };
- }
-
- // componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
- componentDidMount() {
- this.reconnect();
- }
-
- // reconnect establishes a websocket connection with the server, listens for incoming messages
- // and tries to reconnect on connection loss.
- reconnect = () => {
- const server = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/api");
-
- server.onmessage = event => {
- const msg = JSON.parse(event.data);
- if (isNullOrUndefined(msg)) {
- return;
- }
- this.update(msg);
- };
-
- server.onclose = () => {
- setTimeout(this.reconnect, 3000);
- };
- };
-
- // update analyzes the incoming message, and updates the charts' content correspondingly.
- update = msg => {
- console.log(msg);
- this.setState(prevState => {
- let newState = [];
- newState.shouldUpdate = {};
- const insert = (key, values, limit) => {
- newState[key] = [...prevState[key], ...values];
- while (newState[key].length > limit) {
- newState[key].shift();
- }
- newState.shouldUpdate[key] = true;
- };
- // (Re)initialize the state with the past data.
- if (!isNullOrUndefined(msg.history)) {
- const memory = DATA_KEYS.memory;
- const traffic = DATA_KEYS.traffic;
- newState[memory] = [];
- newState[traffic] = [];
- if (!isNullOrUndefined(msg.history.memorySamples)) {
- newState[memory] = msg.history.memorySamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
- while (newState[memory].length > LIMIT.memory) {
- newState[memory].shift();
- }
- newState.shouldUpdate[memory] = true;
- }
- if (!isNullOrUndefined(msg.history.trafficSamples)) {
- newState[traffic] = msg.history.trafficSamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
- while (newState[traffic].length > LIMIT.traffic) {
- newState[traffic].shift();
- }
- newState.shouldUpdate[traffic] = true;
- }
- }
- // Insert the new data samples.
- if (!isNullOrUndefined(msg.memory)) {
- insert(DATA_KEYS.memory, [isNullOrUndefined(msg.memory.value) ? 0 : msg.memory.value], LIMIT.memory);
- }
- if (!isNullOrUndefined(msg.traffic)) {
- insert(DATA_KEYS.traffic, [isNullOrUndefined(msg.traffic.value) ? 0 : msg.traffic.value], LIMIT.traffic);
- }
- if (!isNullOrUndefined(msg.log)) {
- insert(DATA_KEYS.logs, [msg.log], LIMIT.log);
- }
-
- return newState;
- });
- };
-
- // The change of the active label on the SideBar component will trigger a new render in the Main component.
- changeContent = active => {
- this.setState(prevState => prevState.active !== active ? {active: active} : {});
- };
-
- openSideBar = () => {
- this.setState({sideBar: true});
- };
-
- closeSideBar = () => {
- this.setState({sideBar: false});
- };
-
- render() {
- // The classes property is injected by withStyles().
- const {classes} = this.props;
-
- return (
- <div className={classes.appFrame}>
- <Header
- opened={this.state.sideBar}
- open={this.openSideBar}
- />
- <SideBar
- opened={this.state.sideBar}
- close={this.closeSideBar}
- changeContent={this.changeContent}
- />
- <Main
- opened={this.state.sideBar}
- active={this.state.active}
- memory={this.state.memory}
- traffic={this.state.traffic}
- logs={this.state.logs}
- shouldUpdate={this.state.shouldUpdate}
- />
- </div>
- );
- }
-}
-
-Dashboard.propTypes = {
- classes: PropTypes.object.isRequired,
+export type Props = {
+ classes: Object,
};
+type State = {
+ active: string, // active menu
+ sideBar: boolean, // true if the sidebar is opened
+ content: $Shape<Content>, // the visualized data
+ shouldUpdate: Set<string> // labels for the components, which need to rerender based on the incoming message
+};
+// Dashboard is the main component, which renders the whole page, makes connection with the server and
+// listens for messages. When there is an incoming message, updates the page's content correspondingly.
+class Dashboard extends Component<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ active: MENU.get('home').id,
+ sideBar: true,
+ content: {home: {memory: [], traffic: []}, logs: {log: []}},
+ shouldUpdate: new Set(),
+ };
+ }
+
+ // componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
+ componentDidMount() {
+ this.reconnect();
+ }
+
+ // reconnect establishes a websocket connection with the server, listens for incoming messages
+ // and tries to reconnect on connection loss.
+ reconnect = () => {
+ this.setState({
+ content: {home: {memory: [], traffic: []}, logs: {log: []}},
+ });
+ const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`);
+ server.onmessage = (event) => {
+ const msg: Message = JSON.parse(event.data);
+ if (!msg) {
+ return;
+ }
+ this.update(msg);
+ };
+ server.onclose = () => {
+ setTimeout(this.reconnect, 3000);
+ };
+ };
+
+ // samples retrieves the raw data of a chart field from the incoming message.
+ samples = (chart: Chart) => {
+ let s = [];
+ if (chart.history) {
+ s = chart.history.map(({value}) => (value || 0)); // traffic comes without value at the beginning
+ }
+ if (chart.new) {
+ s = [...s, chart.new.value || 0];
+ }
+ return s;
+ };
+
+ // handleHome changes the home-menu related part of the state.
+ handleHome = (home: HomeMessage) => {
+ this.setState((prevState) => {
+ let newState = prevState;
+ newState.shouldUpdate = new Set();
+ if (home.memory) {
+ newState = appender(memoryLens, this.samples(home.memory), SAMPLE.get('memory').limit)(newState);
+ newState.shouldUpdate.add('memory');
+ }
+ if (home.traffic) {
+ newState = appender(trafficLens, this.samples(home.traffic), SAMPLE.get('traffic').limit)(newState);
+ newState.shouldUpdate.add('traffic');
+ }
+ return newState;
+ });
+ };
+
+ // handleLogs changes the logs-menu related part of the state.
+ handleLogs = (logs: LogsMessage) => {
+ this.setState((prevState) => {
+ let newState = prevState;
+ newState.shouldUpdate = new Set();
+ if (logs.log) {
+ newState = appender(logLens, [logs.log], SAMPLE.get('logs').limit)(newState);
+ newState.shouldUpdate.add('logs');
+ }
+ return newState;
+ });
+ };
+
+ // update analyzes the incoming message, and updates the charts' content correspondingly.
+ update = (msg: Message) => {
+ if (msg.home) {
+ this.handleHome(msg.home);
+ }
+ if (msg.logs) {
+ this.handleLogs(msg.logs);
+ }
+ };
+
+ // changeContent sets the active label, which is used at the content rendering.
+ changeContent = (newActive: string) => {
+ this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
+ };
+
+ // openSideBar opens the sidebar.
+ openSideBar = () => {
+ this.setState({sideBar: true});
+ };
+
+ // closeSideBar closes the sidebar.
+ closeSideBar = () => {
+ this.setState({sideBar: false});
+ };
+
+ render() {
+ const {classes} = this.props; // The classes property is injected by withStyles().
+
+ return (
+ <div className={classes.dashboard}>
+ <Header
+ opened={this.state.sideBar}
+ openSideBar={this.openSideBar}
+ closeSideBar={this.closeSideBar}
+ />
+ <Body
+ opened={this.state.sideBar}
+ changeContent={this.changeContent}
+ active={this.state.active}
+ content={this.state.content}
+ shouldUpdate={this.state.shouldUpdate}
+ />
+ </div>
+ );
+ }
+}
export default withStyles(styles)(Dashboard);
diff --git a/dashboard/assets/components/Header.jsx b/dashboard/assets/components/Header.jsx
index 7cf57c9c0..e29507bef 100644
--- a/dashboard/assets/components/Header.jsx
+++ b/dashboard/assets/components/Header.jsx
@@ -1,3 +1,5 @@
+// @flow
+
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -15,73 +17,89 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import {withStyles} from 'material-ui/styles';
+
+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 Transition from 'react-transition-group/Transition';
import IconButton from 'material-ui/IconButton';
-import MenuIcon from 'material-ui-icons/Menu';
+import Typography from 'material-ui/Typography';
+import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
-import {DRAWER_WIDTH} from './Common.jsx';
+import {DURATION} from './Common';
+// arrowDefault is the default style of the arrow button.
+const arrowDefault = {
+ transition: `transform ${DURATION}ms`,
+};
+// arrowTransition is the additional style of the arrow button corresponding to the transition's state.
+const arrowTransition = {
+ entered: {transform: 'rotate(180deg)'},
+};
// Styles for the Header component.
const styles = theme => ({
- appBar: {
- position: 'absolute',
- transition: theme.transitions.create(['margin', 'width'], {
- easing: theme.transitions.easing.sharp,
- duration: theme.transitions.duration.leavingScreen,
- }),
- },
- appBarShift: {
- marginLeft: DRAWER_WIDTH,
- width: `calc(100% - ${DRAWER_WIDTH}px)`,
- transition: theme.transitions.create(['margin', 'width'], {
- easing: theme.transitions.easing.easeOut,
- duration: theme.transitions.duration.enteringScreen,
- }),
- },
- menuButton: {
- marginLeft: 12,
- marginRight: 20,
- },
- hide: {
- display: 'none',
- },
+ header: {
+ backgroundColor: theme.palette.background.appBar,
+ color: theme.palette.getContrastText(theme.palette.background.appBar),
+ zIndex: theme.zIndex.appBar,
+ },
+ toolbar: {
+ paddingLeft: theme.spacing.unit,
+ paddingRight: theme.spacing.unit,
+ },
+ mainText: {
+ paddingLeft: theme.spacing.unit,
+ },
});
+export type Props = {
+ classes: Object,
+ opened: boolean,
+ openSideBar: () => {},
+ closeSideBar: () => {},
+};
+// Header renders the header of the dashboard.
+class Header extends Component<Props> {
+ shouldComponentUpdate(nextProps) {
+ return nextProps.opened !== this.props.opened;
+ }
-// Header renders a header, which contains a sidebar opener icon when that is closed.
-class Header extends Component {
- render() {
- // The classes property is injected by withStyles().
- const {classes} = this.props;
+ // changeSideBar opens or closes the sidebar corresponding to the previous state.
+ changeSideBar = () => {
+ if (this.props.opened) {
+ this.props.closeSideBar();
+ } else {
+ this.props.openSideBar();
+ }
+ };
- return (
- <AppBar className={classNames(classes.appBar, this.props.opened && classes.appBarShift)}>
- <Toolbar disableGutters={!this.props.opened}>
- <IconButton
- color="contrast"
- aria-label="open drawer"
- onClick={this.props.open}
- className={classNames(classes.menuButton, this.props.opened && classes.hide)}
- >
- <MenuIcon />
- </IconButton>
- <Typography type="title" color="inherit" noWrap>
- Go Ethereum Dashboard
- </Typography>
- </Toolbar>
- </AppBar>
- );
- }
-}
+ // arrowButton is connected to the sidebar; changes its state.
+ arrowButton = (transitionState: string) => (
+ <IconButton onClick={this.changeSideBar}>
+ <ChevronLeftIcon
+ style={{
+ ...arrowDefault,
+ ...arrowTransition[transitionState],
+ }}
+ />
+ </IconButton>
+ );
-Header.propTypes = {
- classes: PropTypes.object.isRequired,
- opened: PropTypes.bool.isRequired,
- open: PropTypes.func.isRequired,
-};
+ render() {
+ const {classes, opened} = this.props; // The classes property is injected by withStyles().
+
+ return (
+ <AppBar position="static" className={classes.header}>
+ <Toolbar className={classes.toolbar}>
+ <Transition mountOnEnter in={opened} timeout={{enter: DURATION}}>
+ {this.arrowButton}
+ </Transition>
+ <Typography type="title" color="inherit" noWrap className={classes.mainText}>
+ Go Ethereum Dashboard
+ </Typography>
+ </Toolbar>
+ </AppBar>
+ );
+ }
+}
export default withStyles(styles)(Header);
diff --git a/dashboard/assets/components/Home.jsx b/dashboard/assets/components/Home.jsx
index f67bac555..d3e1004f9 100644
--- a/dashboard/assets/components/Home.jsx
+++ b/dashboard/assets/components/Home.jsx
@@ -1,3 +1,5 @@
+// @flow
+
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -15,75 +17,56 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import Grid from 'material-ui/Grid';
-import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line, ResponsiveContainer} from 'recharts';
-import {withTheme} from 'material-ui/styles';
-import {isNullOrUndefined, DATA_KEYS} from "./Common.jsx";
+import withTheme from 'material-ui/styles/withTheme';
+import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line} from 'recharts';
-// ChartGrid renders a grid container for responsive charts.
-// The children are Recharts components extended with the Material-UI's xs property.
-class ChartGrid extends Component {
- render() {
- return (
- <Grid container spacing={this.props.spacing}>
- {
- React.Children.map(this.props.children, child => (
- <Grid item xs={child.props.xs}>
- <ResponsiveContainer width="100%" height={child.props.height}>
- {React.cloneElement(child, {data: child.props.values.map(value => ({value: value}))})}
- </ResponsiveContainer>
- </Grid>
- ))
- }
- </Grid>
- );
- }
-}
+import ChartGrid from './ChartGrid';
+import type {ChartEntry} from '../types/message';
-ChartGrid.propTypes = {
- spacing: PropTypes.number.isRequired,
+export type Props = {
+ theme: Object,
+ memory: Array<ChartEntry>,
+ traffic: Array<ChartEntry>,
+ shouldUpdate: Object,
};
+// Home renders the home content.
+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];
+ }
-// Home renders the home component.
-class Home extends Component {
- shouldComponentUpdate(nextProps) {
- return !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.memory]) ||
- !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.traffic]);
- }
+ shouldComponentUpdate(nextProps) {
+ return nextProps.shouldUpdate.has('memory') || nextProps.shouldUpdate.has('traffic');
+ }
- render() {
- const {theme} = this.props;
- const memoryColor = theme.palette.primary[300];
- const trafficColor = theme.palette.secondary[300];
+ render() {
+ const {memory, traffic} = this.props;
- return (
- <ChartGrid spacing={24}>
- <AreaChart xs={6} height={300} values={this.props.memory}>
- <YAxis />
- <Area type="monotone" dataKey="value" stroke={memoryColor} fill={memoryColor} />
- </AreaChart>
- <LineChart xs={6} height={300} values={this.props.traffic}>
- <Line type="monotone" dataKey="value" stroke={trafficColor} dot={false} />
- </LineChart>
- <LineChart xs={6} height={300} values={this.props.memory}>
- <YAxis />
- <CartesianGrid stroke="#eee" strokeDasharray="5 5" />
- <Line type="monotone" dataKey="value" stroke={memoryColor} dot={false} />
- </LineChart>
- <AreaChart xs={6} height={300} values={this.props.traffic}>
- <CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
- <Area type="monotone" dataKey="value" stroke={trafficColor} fill={trafficColor} />
- </AreaChart>
- </ChartGrid>
- );
- }
+ return (
+ <ChartGrid spacing={24}>
+ <AreaChart xs={6} height={300} values={memory}>
+ <YAxis />
+ <Area type="monotone" dataKey="value" stroke={this.memoryColor} fill={this.memoryColor} />
+ </AreaChart>
+ <LineChart xs={6} height={300} values={traffic}>
+ <Line type="monotone" dataKey="value" stroke={this.trafficColor} dot={false} />
+ </LineChart>
+ <LineChart xs={6} height={300} values={memory}>
+ <YAxis />
+ <CartesianGrid stroke="#eee" strokeDasharray="5 5" />
+ <Line type="monotone" dataKey="value" stroke={this.memoryColor} dot={false} />
+ </LineChart>
+ <AreaChart xs={6} height={300} values={traffic}>
+ <CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
+ <Area type="monotone" dataKey="value" stroke={this.trafficColor} fill={this.trafficColor} />
+ </AreaChart>
+ </ChartGrid>
+ );
+ }
}
-Home.propTypes = {
- theme: PropTypes.object.isRequired,
- shouldUpdate: PropTypes.object.isRequired,
-};
-
export default withTheme()(Home);
diff --git a/dashboard/assets/components/Main.jsx b/dashboard/assets/components/Main.jsx
index b119d1ffd..6f1668a29 100644
--- a/dashboard/assets/components/Main.jsx
+++ b/dashboard/assets/components/Main.jsx
@@ -1,3 +1,5 @@
+// @flow
+
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -15,95 +17,52 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import {withStyles} from 'material-ui/styles';
-
-import {TAGS, DRAWER_WIDTH} from "./Common.jsx";
-import Home from './Home.jsx';
-// ContentSwitch chooses and renders the proper page content.
-class ContentSwitch extends Component {
- render() {
- switch(this.props.active) {
- case TAGS.home.id:
- return <Home memory={this.props.memory} traffic={this.props.traffic} shouldUpdate={this.props.shouldUpdate} />;
- case TAGS.chain.id:
- return null;
- case TAGS.transactions.id:
- return null;
- case TAGS.network.id:
- // Only for testing.
- return null;
- case TAGS.system.id:
- return null;
- case TAGS.logs.id:
- return <div>{this.props.logs.map((log, index) => <div key={index}>{log}</div>)}</div>;
- }
- return null;
- }
-}
+import withStyles from 'material-ui/styles/withStyles';
-ContentSwitch.propTypes = {
- active: PropTypes.string.isRequired,
- shouldUpdate: PropTypes.object.isRequired,
-};
+import Home from './Home';
+import {MENU} from './Common';
+import type {Content} from '../types/content';
-// styles contains the styles for the Main component.
+// Styles for the Content component.
const styles = theme => ({
- content: {
- width: '100%',
- marginLeft: -DRAWER_WIDTH,
- flexGrow: 1,
- backgroundColor: theme.palette.background.default,
- padding: theme.spacing.unit * 3,
- transition: theme.transitions.create('margin', {
- easing: theme.transitions.easing.sharp,
- duration: theme.transitions.duration.leavingScreen,
- }),
- marginTop: 56,
- overflow: 'auto',
- [theme.breakpoints.up('sm')]: {
- content: {
- height: 'calc(100% - 64px)',
- marginTop: 64,
- },
- },
- },
- contentShift: {
- marginLeft: 0,
- transition: theme.transitions.create('margin', {
- easing: theme.transitions.easing.easeOut,
- duration: theme.transitions.duration.enteringScreen,
- }),
- },
+ content: {
+ flexGrow: 1,
+ backgroundColor: theme.palette.background.default,
+ padding: theme.spacing.unit * 3,
+ overflow: 'auto',
+ },
});
+export type Props = {
+ classes: Object,
+ active: string,
+ content: Content,
+ shouldUpdate: Object,
+};
+// Main renders the chosen content.
+class Main extends Component<Props> {
+ render() {
+ const {
+ classes, active, content, shouldUpdate,
+ } = this.props;
-// Main renders a component for the page content.
-class Main extends Component {
- render() {
- // The classes property is injected by withStyles().
- const {classes} = this.props;
+ let children = null;
+ switch (active) {
+ case MENU.get('home').id:
+ children = <Home memory={content.home.memory} traffic={content.home.traffic} shouldUpdate={shouldUpdate} />;
+ break;
+ case MENU.get('chain').id:
+ case MENU.get('txpool').id:
+ case MENU.get('network').id:
+ case MENU.get('system').id:
+ children = <div>Work in progress.</div>;
+ break;
+ case MENU.get('logs').id:
+ children = <div>{content.logs.log.map((log, index) => <div key={index}>{log}</div>)}</div>;
+ }
- return (
- <main className={classNames(classes.content, this.props.opened && classes.contentShift)}>
- <ContentSwitch
- active={this.props.active}
- memory={this.props.memory}
- traffic={this.props.traffic}
- logs={this.props.logs}
- shouldUpdate={this.props.shouldUpdate}
- />
- </main>
- );
- }
+ return <div className={classes.content}>{children}</div>;
+ }
}
-Main.propTypes = {
- classes: PropTypes.object.isRequired,
- opened: PropTypes.bool.isRequired,
- active: PropTypes.string.isRequired,
- shouldUpdate: PropTypes.object.isRequired,
-};
-
export default withStyles(styles)(Main);
diff --git a/dashboard/assets/components/SideBar.jsx b/dashboard/assets/components/SideBar.jsx
index ef077f1e0..319e6f305 100644
--- a/dashboard/assets/components/SideBar.jsx
+++ b/dashboard/assets/components/SideBar.jsx
@@ -1,3 +1,5 @@
+// @flow
+
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@@ -15,92 +17,106 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withStyles} from 'material-ui/styles';
-import Drawer from 'material-ui/Drawer';
-import {IconButton} from "material-ui";
-import List, {ListItem, ListItemText} from 'material-ui/List';
-import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
-import {TAGS, DRAWER_WIDTH} from './Common.jsx';
+import withStyles from 'material-ui/styles/withStyles';
+import List, {ListItem, ListItemIcon, ListItemText} from 'material-ui/List';
+import Icon from 'material-ui/Icon';
+import Transition from 'react-transition-group/Transition';
+import {Icon as FontAwesome} from 'react-fa';
+
+import {MENU, DURATION} from './Common';
+// menuDefault is the default style of the menu.
+const menuDefault = {
+ transition: `margin-left ${DURATION}ms`,
+};
+// menuTransition is the additional style of the menu corresponding to the transition's state.
+const menuTransition = {
+ entered: {marginLeft: -200},
+};
// Styles for the SideBar component.
const styles = theme => ({
- drawerPaper: {
- position: 'relative',
- height: '100%',
- width: DRAWER_WIDTH,
- },
- drawerHeader: {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'flex-end',
- padding: '0 8px',
- ...theme.mixins.toolbar,
- transitionDuration: {
- enter: theme.transitions.duration.enteringScreen,
- exit: theme.transitions.duration.leavingScreen,
- }
- },
+ list: {
+ background: theme.palette.background.appBar,
+ },
+ listItem: {
+ minWidth: theme.spacing.unit * 3,
+ },
+ icon: {
+ fontSize: theme.spacing.unit * 3,
+ },
});
+export type Props = {
+ classes: Object,
+ opened: boolean,
+ changeContent: () => {},
+};
+// SideBar renders the sidebar of the dashboard.
+class SideBar extends Component<Props> {
+ constructor(props) {
+ super(props);
-// SideBar renders a sidebar component.
-class SideBar extends Component {
- 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);
+ };
+ });
+ }
- // 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 = {};
- for(let key in TAGS) {
- const id = TAGS[key].id;
- this.clickOn[id] = event => {
- event.preventDefault();
- console.log(event.target.key);
- this.props.changeContent(id);
- };
- }
- }
+ shouldComponentUpdate(nextProps) {
+ return nextProps.opened !== this.props.opened;
+ }
- render() {
- // The classes property is injected by withStyles().
- const {classes} = this.props;
+ menuItems = (transitionState) => {
+ const {classes} = this.props;
+ const children = [];
+ MENU.forEach((menu) => {
+ children.push(
+ <ListItem button key={menu.id} onClick={this.clickOn[menu.id]} className={classes.listItem}>
+ <ListItemIcon>
+ <Icon className={classes.icon}>
+ <FontAwesome name={menu.icon} />
+ </Icon>
+ </ListItemIcon>
+ <ListItemText
+ primary={menu.title}
+ style={{
+ ...menuDefault,
+ ...menuTransition[transitionState],
+ padding: 0,
+ }}
+ />
+ </ListItem>,
+ );
+ });
+ return children;
+ };
- return (
- <Drawer
- type="persistent"
- classes={{paper: classes.drawerPaper,}}
- open={this.props.opened}
- >
- <div>
- <div className={classes.drawerHeader}>
- <IconButton onClick={this.props.close}>
- <ChevronLeftIcon />
- </IconButton>
- </div>
- <List>
- {
- Object.values(TAGS).map(tag => {
- return (
- <ListItem button key={tag.id} onClick={this.clickOn[tag.id]}>
- <ListItemText primary={tag.title} />
- </ListItem>
- );
- })
- }
- </List>
- </div>
- </Drawer>
- );
- }
-}
+ // menu renders the list of the menu items.
+ menu = (transitionState) => {
+ const {classes} = this.props; // The classes property is injected by withStyles().
-SideBar.propTypes = {
- classes: PropTypes.object.isRequired,
- opened: PropTypes.bool.isRequired,
- close: PropTypes.func.isRequired,
- changeContent: PropTypes.func.isRequired,
-};
+ return (
+ <div className={classes.list}>
+ <List>
+ {this.menuItems(transitionState)}
+ </List>
+ </div>
+ );
+ };
+
+ render() {
+ return (
+ <Transition mountOnEnter in={this.props.opened} timeout={{enter: DURATION}}>
+ {this.menu}
+ </Transition>
+ );
+ }
+}
export default withStyles(styles)(SideBar);