diff options
author | Kurkó Mihály <kurkomisi@users.noreply.github.com> | 2018-01-24 04:51:04 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2018-01-24 04:51:04 +0800 |
commit | 05ade19302357eba6a24348f31df140ce0eca326 (patch) | |
tree | 50010a6f94401d7cc1829d36ea99d342d2825d39 /dashboard/assets/components | |
parent | ec96216d1696bca2671bb7d043ba6af02c20738d (diff) | |
download | dexon-05ade19302357eba6a24348f31df140ce0eca326.tar dexon-05ade19302357eba6a24348f31df140ce0eca326.tar.gz dexon-05ade19302357eba6a24348f31df140ce0eca326.tar.bz2 dexon-05ade19302357eba6a24348f31df140ce0eca326.tar.lz dexon-05ade19302357eba6a24348f31df140ce0eca326.tar.xz dexon-05ade19302357eba6a24348f31df140ce0eca326.tar.zst dexon-05ade19302357eba6a24348f31df140ce0eca326.zip |
dashboard: CPU, memory, diskIO and traffic on the footer (#15950)
* dashboard: footer, deep state update
* dashboard: resolve asset path
* dashboard: prevent state update on every reconnection
* dashboard: fix linter issue
* dashboard, cmd: minor UI fix, include commit hash
* dashboard: gitCommit renamed to commit
* dashboard: move the geth version to the right, make commit optional
* dashboard: memory, traffic and CPU on footer
* dashboard: fix merge
* dashboard: CPU, diskIO on footer
* dashboard: rename variables, use group declaration
* dashboard: docs
Diffstat (limited to 'dashboard/assets/components')
-rw-r--r-- | dashboard/assets/components/Body.jsx | 19 | ||||
-rw-r--r-- | dashboard/assets/components/ChartRow.jsx (renamed from dashboard/assets/components/ChartGrid.jsx) | 46 | ||||
-rw-r--r-- | dashboard/assets/components/Common.jsx | 65 | ||||
-rw-r--r-- | dashboard/assets/components/CustomTooltip.jsx | 95 | ||||
-rw-r--r-- | dashboard/assets/components/Dashboard.jsx | 109 | ||||
-rw-r--r-- | dashboard/assets/components/Footer.jsx | 163 | ||||
-rw-r--r-- | dashboard/assets/components/Header.jsx | 62 | ||||
-rw-r--r-- | dashboard/assets/components/Home.jsx | 77 | ||||
-rw-r--r-- | dashboard/assets/components/Main.jsx | 39 | ||||
-rw-r--r-- | dashboard/assets/components/SideBar.jsx | 84 |
10 files changed, 413 insertions, 346 deletions
diff --git a/dashboard/assets/components/Body.jsx b/dashboard/assets/components/Body.jsx index 14e9ac358..054e04064 100644 --- a/dashboard/assets/components/Body.jsx +++ b/dashboard/assets/components/Body.jsx @@ -18,35 +18,32 @@ 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 = () => ({ +// styles contains the constant styles of the component. +const styles = { body: { display: 'flex', width: '100%', height: '100%', }, -}); +}; + export type Props = { - classes: Object, opened: boolean, - changeContent: () => {}, + changeContent: string => void, 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}> + <div style={styles.body}> <SideBar opened={this.props.opened} changeContent={this.props.changeContent} @@ -61,4 +58,4 @@ class Body extends Component<Props> { } } -export default withStyles(styles)(Body); +export default Body; diff --git a/dashboard/assets/components/ChartGrid.jsx b/dashboard/assets/components/ChartRow.jsx index 45dde7499..1075346fe 100644 --- a/dashboard/assets/components/ChartGrid.jsx +++ b/dashboard/assets/components/ChartRow.jsx @@ -1,6 +1,6 @@ // @flow -// Copyright 2017 The go-ethereum Authors +// 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 @@ -17,33 +17,41 @@ // 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 type {ChildrenArray} from 'react'; import Grid from 'material-ui/Grid'; -import {ResponsiveContainer} from 'recharts'; + +// 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 = { - spacing: number, - children: Node, + children: ChildrenArray<React$Element<any>>, }; -// 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> { + +// ChartRow renders a row of equally sized responsive charts. +class ChartRow 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 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 ChartGrid; +export default ChartRow; diff --git a/dashboard/assets/components/Common.jsx b/dashboard/assets/components/Common.jsx deleted file mode 100644 index 256a3e661..000000000 --- a/dashboard/assets/components/Common.jsx +++ /dev/null @@ -1,65 +0,0 @@ -// @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/>. - -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. -// 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}]))); - -export const DURATION = 200; diff --git a/dashboard/assets/components/CustomTooltip.jsx b/dashboard/assets/components/CustomTooltip.jsx new file mode 100644 index 000000000..be7c624cf --- /dev/null +++ b/dashboard/assets/components/CustomTooltip.jsx @@ -0,0 +1,95 @@ +// @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; + +// percentPlotter renders a tooltip, which displays the value of the payload followed by a percent sign. +export const percentPlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => { + const p = mapper(payload); + if (typeof p !== 'number') { + return null; + } + return ( + <Typography type='caption' color='inherit'> + <span style={styles.light}>{text}</span> {p.toFixed(2)} % + </Typography> + ); +}; + +// unit contains the units for the bytePlotter. +const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + +// simplifyBytes returns the simplified version of the given value followed by the unit. +const simplifyBytes = (x: number) => { + let i = 0; + for (; x > 1024 && i < 5; i++) { + x /= 1024; + } + return x.toFixed(2).toString().concat(' ', unit[i]); +}; + +// bytePlotter renders a tooltip, which displays the payload as a byte value. +export const bytePlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => { + const p = mapper(payload); + if (typeof p !== 'number') { + return null; + } + return ( + <Typography type='caption' color='inherit'> + <span style={styles.light}>{text}</span> {simplifyBytes(p)} + </Typography> + ); +}; + +// bytePlotter renders a tooltip, which displays the payload as a byte value followed by '/s'. +export const bytePerSecPlotter = <T>(text: string, mapper: (T => T) = multiplier(1)) => (payload: T) => { + const p = mapper(payload); + if (typeof p !== 'number') { + return null; + } + return ( + <Typography type='caption' color='inherit'> + <span style={styles.light}>{text}</span> {simplifyBytes(p)}/s + </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; diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index 036dd050b..90b1a785c 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -22,8 +22,7 @@ import withStyles from 'material-ui/styles/withStyles'; import Header from './Header'; import Body from './Body'; -import Footer from './Footer'; -import {MENU} from './Common'; +import {MENU} from '../common'; import type {Content} from '../types/content'; // 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 // handler function for every path of the state in order to maximize the flexibility // 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') { // TODO (kurkomisi): originally this was deep copy, investigate it. return prev; } if (typeof updater === 'function') { - return updater(prev, update); + return updater(update, prev); } const updated = {}; 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; @@ -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. // // 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 = {}; 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; }; -// appender is a state update generalization function, which appends the update data -// to the existing data. limit defines the maximum allowed size of the created array. -const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit); +// replacer is a state updater function, which replaces the original data. +const replacer = <T>(update: T) => update; -// replacer is a state update generalization function, which replaces the original data. -const replacer = <T>(prev: T, update: T) => update; +// appender is a state updater function, which appends the update data to the +// 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. const defaultContent: Content = { @@ -79,8 +82,14 @@ const defaultContent: Content = { commit: null, }, home: { - memory: [], - traffic: [], + activeMemory: [], + virtualMemory: [], + networkIngress: [], + networkEgress: [], + processCPU: [], + systemCPU: [], + diskRead: [], + diskWrite: [], }, chain: {}, txpool: {}, @@ -91,16 +100,23 @@ const defaultContent: Content = { }, }; -// updaters contains the state update generalization functions for each path of the state. -// TODO (kurkomisi): Define a tricky type which embraces the content and the handlers. +// updaters contains the state updater functions for each path of the state. +// +// TODO (kurkomisi): Define a tricky type which embraces the content and the updaters. const updaters = { general: { version: replacer, commit: replacer, }, home: { - memory: appender(200), - traffic: appender(200), + activeMemory: appender(200), + virtualMemory: appender(200), + networkIngress: appender(200), + networkEgress: appender(200), + processCPU: appender(200), + systemCPU: appender(200), + diskRead: appender(200), + diskWrite: appender(200), }, chain: null, txpool: null, @@ -111,28 +127,34 @@ const updaters = { }, }; -// styles returns the styles for the Dashboard component. -const styles = theme => ({ +// styles contains the constant styles of the component. +const styles = { + dashboard: { + display: 'flex', + flexFlow: 'column', + width: '100%', + height: '100%', + zIndex: 1, + overflow: 'hidden', + } +}; + +// themeStyles returns the styles generated from the theme for the component. +const themeStyles: Object = (theme: Object) => ({ dashboard: { - display: 'flex', - flexFlow: 'column', - width: '100%', - height: '100%', background: theme.palette.background.default, - zIndex: 1, - overflow: 'hidden', }, }); export type Props = { - classes: Object, + classes: Object, // injected by withStyles() }; type State = { active: string, // active menu sideBar: boolean, // true if the sidebar is opened 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 @@ -176,8 +198,8 @@ class Dashboard extends Component<Props, State> { // update updates the content corresponding to the incoming message. update = (msg: $Shape<Content>) => { this.setState(prevState => ({ - content: deepUpdate(prevState.content, msg, updaters), - shouldUpdate: shouldUpdate(msg, updaters), + content: deepUpdate(updaters, msg, prevState.content), + shouldUpdate: shouldUpdate(updaters, msg), })); }; @@ -186,25 +208,17 @@ class Dashboard extends Component<Props, State> { 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}); + // switchSideBar opens or closes the sidebar's state. + switchSideBar = () => { + this.setState(prevState => ({sideBar: !prevState.sideBar})); }; render() { - const {classes} = this.props; // The classes property is injected by withStyles(). - return ( - <div className={classes.dashboard}> + <div className={this.props.classes.dashboard} style={styles.dashboard}> <Header opened={this.state.sideBar} - openSideBar={this.openSideBar} - closeSideBar={this.closeSideBar} + switchSideBar={this.switchSideBar} /> <Body opened={this.state.sideBar} @@ -213,16 +227,9 @@ class Dashboard extends Component<Props, State> { content={this.state.content} 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> ); } } -export default withStyles(styles)(Dashboard); +export default withStyles(themeStyles)(Dashboard); diff --git a/dashboard/assets/components/Footer.jsx b/dashboard/assets/components/Footer.jsx index 7130b4e4e..54b67c464 100644 --- a/dashboard/assets/components/Footer.jsx +++ b/dashboard/assets/components/Footer.jsx @@ -19,62 +19,155 @@ import React, {Component} from 'react'; 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 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, bytePerSecPlotter, percentPlotter, multiplier} from './CustomTooltip'; +import {styles as commonStyles} from '../common'; +import type {Content} from '../types/content'; -// styles contains styles for the Header component. -const styles = theme => ({ +// styles contains the constant styles of the component. +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: { 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, - display: 'flex', - justifyContent: 'flex-end', - }, - light: { - color: 'rgba(255, 255, 255, 0.54)', + height: theme.spacing.unit * 10, }, }); + export type Props = { - general: General, - classes: Object, + classes: Object, // injected by withStyles() + 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> { shouldComponentUpdate(nextProps) { - return typeof nextProps.shouldUpdate.logs !== 'undefined'; + return typeof nextProps.shouldUpdate.home !== 'undefined'; } - info = (about: string, data: string) => ( - <Typography type="caption" color="inherit"> - <span className={this.props.classes.light}>{about}</span> {data} + // info renders a label with the given values. + info = (about: string, value: ?string) => (value ? ( + <Typography type='caption' color='inherit'> + <span style={commonStyles.light}>{about}</span> {value} </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() { - const {classes, general} = this.props; // The classes property is injected by withStyles(). - const geth = general.version ? this.info('Geth', general.version) : null; - const commit = general.commit ? this.info('Commit', general.commit.substring(0, 7)) : null; + const {content} = this.props; + const {general, home} = content; return ( - <AppBar position="static" className={classes.footer}> - <Toolbar className={classes.toolbar}> - <div> - {geth} - {commit} - </div> - </Toolbar> - </AppBar> + <Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}> + <Grid item xs style={styles.chartRowWrapper}> + <ChartRow> + {this.doubleChart( + 'all', + {data: home.processCPU, tooltip: percentPlotter('Process')}, + {data: home.systemCPU, tooltip: percentPlotter('System', multiplier(-1))}, + )} + {this.doubleChart( + 'all', + {data: home.activeMemory, tooltip: bytePlotter('Active')}, + {data: home.virtualMemory, tooltip: bytePlotter('Virtual', multiplier(-1))}, + )} + {this.doubleChart( + 'all', + {data: home.diskRead, tooltip: bytePerSecPlotter('Disk Read')}, + {data: home.diskWrite, tooltip: bytePerSecPlotter('Disk Write', multiplier(-1))}, + )} + {this.doubleChart( + 'all', + {data: home.networkIngress, tooltip: bytePerSecPlotter('Download')}, + {data: home.networkEgress, tooltip: bytePerSecPlotter('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); diff --git a/dashboard/assets/components/Header.jsx b/dashboard/assets/components/Header.jsx index e29507bef..e91885af3 100644 --- a/dashboard/assets/components/Header.jsx +++ b/dashboard/assets/components/Header.jsx @@ -26,18 +26,22 @@ import IconButton from 'material-ui/IconButton'; import Typography from 'material-ui/Typography'; import ChevronLeftIcon from 'material-ui-icons/ChevronLeft'; -import {DURATION} from './Common'; +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 contains the constant styles of the component. +const styles = { + arrow: { + default: { + transition: `transform ${DURATION}ms`, + }, + transition: { + 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: { backgroundColor: theme.palette.background.appBar, color: theme.palette.getContrastText(theme.palette.background.appBar), @@ -47,53 +51,45 @@ const styles = theme => ({ paddingLeft: theme.spacing.unit, paddingRight: theme.spacing.unit, }, - mainText: { + title: { paddingLeft: theme.spacing.unit, }, }); + export type Props = { - classes: Object, + classes: Object, // injected by withStyles() opened: boolean, - openSideBar: () => {}, - closeSideBar: () => {}, + switchSideBar: () => void, }; + // Header renders the header of the dashboard. class Header extends Component<Props> { shouldComponentUpdate(nextProps) { return nextProps.opened !== this.props.opened; } - // changeSideBar opens or closes the sidebar corresponding to the previous state. - changeSideBar = () => { - if (this.props.opened) { - this.props.closeSideBar(); - } else { - this.props.openSideBar(); - } - }; - - // arrowButton is connected to the sidebar; changes its state. - arrowButton = (transitionState: string) => ( - <IconButton onClick={this.changeSideBar}> + // arrow renders a button, which changes the sidebar's state. + arrow = (transitionState: string) => ( + <IconButton onClick={this.props.switchSideBar}> <ChevronLeftIcon style={{ - ...arrowDefault, - ...arrowTransition[transitionState], + ...styles.arrow.default, + ...styles.arrow.transition[transitionState], }} /> </IconButton> ); render() { - const {classes, opened} = this.props; // The classes property is injected by withStyles(). + const {classes, opened} = this.props; return ( - <AppBar position="static" className={classes.header}> + <AppBar position='static' className={classes.header}> <Toolbar className={classes.toolbar}> <Transition mountOnEnter in={opened} timeout={{enter: DURATION}}> - {this.arrowButton} + {this.arrow} </Transition> - <Typography type="title" color="inherit" noWrap className={classes.mainText}> + <Typography type='title' color='inherit' noWrap className={classes.title}> Go Ethereum Dashboard </Typography> </Toolbar> @@ -102,4 +98,4 @@ class Header extends Component<Props> { } } -export default withStyles(styles)(Header); +export default withStyles(themeStyles)(Header); diff --git a/dashboard/assets/components/Home.jsx b/dashboard/assets/components/Home.jsx deleted file mode 100644 index f9fd7bf46..000000000 --- a/dashboard/assets/components/Home.jsx +++ /dev/null @@ -1,77 +0,0 @@ -// @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 withTheme from 'material-ui/styles/withTheme'; -import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line} from 'recharts'; - -import ChartGrid from './ChartGrid'; -import type {ChartEntry} from '../types/content'; - -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]; - } - - shouldComponentUpdate(nextProps) { - return typeof nextProps.shouldUpdate.home !== 'undefined'; - } - - memoryColor: Object; - trafficColor: Object; - - render() { - let {memory, traffic} = this.props; - memory = memory.map(({value}) => (value || 0)); - traffic = traffic.map(({value}) => (value || 0)); - - 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> - ); - } -} - -export default withTheme()(Home); diff --git a/dashboard/assets/components/Main.jsx b/dashboard/assets/components/Main.jsx index 6f1668a29..a9e3d3578 100644 --- a/dashboard/assets/components/Main.jsx +++ b/dashboard/assets/components/Main.jsx @@ -20,25 +20,38 @@ import React, {Component} from 'react'; import withStyles from 'material-ui/styles/withStyles'; -import Home from './Home'; -import {MENU} from './Common'; +import {MENU} from '../common'; +import Footer from './Footer'; import type {Content} from '../types/content'; -// Styles for the Content component. -const styles = theme => ({ +// styles contains the constant styles of the component. +const styles = { + wrapper: { + display: 'flex', + flexDirection: 'column', + width: '100%', + }, + content: { + flex: 1, + overflow: 'auto', + }, +}; + +// themeStyles returns the styles generated from the theme for the component. +const themeStyles = theme => ({ 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() { @@ -49,8 +62,6 @@ class Main extends Component<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: @@ -61,8 +72,16 @@ class Main extends Component<Props> { 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); diff --git a/dashboard/assets/components/SideBar.jsx b/dashboard/assets/components/SideBar.jsx index 319e6f305..c2e419ae9 100644 --- a/dashboard/assets/components/SideBar.jsx +++ b/dashboard/assets/components/SideBar.jsx @@ -24,18 +24,22 @@ 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'; +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 contains the constant styles of the component. +const styles = { + menu: { + default: { + transition: `margin-left ${DURATION}ms`, + }, + transition: { + 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: { background: theme.palette.background.appBar, }, @@ -46,38 +50,32 @@ const styles = theme => ({ fontSize: theme.spacing.unit * 3, }, }); + export type Props = { - classes: Object, + classes: Object, // injected by withStyles() opened: boolean, - changeContent: () => {}, + changeContent: string => void, }; + // SideBar renders the sidebar of the dashboard. 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) { 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) => { const {classes} = this.props; const children = []; MENU.forEach((menu) => { - children.push( - <ListItem button key={menu.id} onClick={this.clickOn[menu.id]} className={classes.listItem}> + children.push(( + <ListItem button key={menu.id} onClick={this.clickOn(menu.id)} className={classes.listItem}> <ListItemIcon> <Icon className={classes.icon}> <FontAwesome name={menu.icon} /> @@ -86,29 +84,25 @@ class SideBar extends Component<Props> { <ListItemText primary={menu.title} style={{ - ...menuDefault, - ...menuTransition[transitionState], + ...styles.menu.default, + ...styles.menu.transition[transitionState], padding: 0, }} /> - </ListItem>, - ); + </ListItem> + )); }); return children; }; // menu renders the list of the menu items. - menu = (transitionState) => { - const {classes} = this.props; // The classes property is injected by withStyles(). - - return ( - <div className={classes.list}> - <List> - {this.menuItems(transitionState)} - </List> - </div> - ); - }; + menu = (transitionState: Object) => ( + <div className={this.props.classes.list}> + <List> + {this.menuItems(transitionState)} + </List> + </div> + ); render() { return ( @@ -119,4 +113,4 @@ class SideBar extends Component<Props> { } } -export default withStyles(styles)(SideBar); +export default withStyles(themeStyles)(SideBar); |