diff options
author | Kurkó Mihály <kurkomisi@users.noreply.github.com> | 2017-12-21 23:54:38 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2017-12-21 23:54:38 +0800 |
commit | 9dbb8ef4aadb8e40aef8b681cf86acd20789abdc (patch) | |
tree | c020de9b45dffa878b1422dce147d9343ed8b59b /dashboard/assets/components | |
parent | 52f4d6dd7891191a494f37faa6bce664e202da66 (diff) | |
download | dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.tar dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.tar.gz dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.tar.bz2 dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.tar.lz dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.tar.xz dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.tar.zst dexon-9dbb8ef4aadb8e40aef8b681cf86acd20789abdc.zip |
dashboard: integrate Flow, sketch message API (#15713)
* dashboard: minor design change
* dashboard: Flow integration, message API
* dashboard: minor polishes, exclude misspell linter
Diffstat (limited to 'dashboard/assets/components')
-rw-r--r-- | dashboard/assets/components/Body.jsx | 64 | ||||
-rw-r--r-- | dashboard/assets/components/ChartGrid.jsx | 49 | ||||
-rw-r--r-- | dashboard/assets/components/Common.jsx | 107 | ||||
-rw-r--r-- | dashboard/assets/components/Dashboard.jsx | 320 | ||||
-rw-r--r-- | dashboard/assets/components/Header.jsx | 132 | ||||
-rw-r--r-- | dashboard/assets/components/Home.jsx | 107 | ||||
-rw-r--r-- | dashboard/assets/components/Main.jsx | 123 | ||||
-rw-r--r-- | dashboard/assets/components/SideBar.jsx | 170 |
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); |