dashboard: minor design change

This commit is contained in:
Kurkó Mihály 2017-11-22 12:46:18 +02:00
parent 049797d40a
commit ff5987e597
17 changed files with 38813 additions and 344 deletions

View file

@ -22,10 +22,9 @@ $ (cd dashboard/assets && ./node_modules/.bin/webpack --watch)
$ geth --dashboard --dashboard.assets=dashboard/assets/public --vmodule=dashboard=5 $ geth --dashboard --dashboard.assets=dashboard/assets/public --vmodule=dashboard=5
``` ```
To bundle up the final UI into Geth, run `webpack` and `go generate`: To bundle up the final UI into Geth, run `go generate`:
``` ```
$ (cd dashboard/assets && ./node_modules/.bin/webpack)
$ go generate ./dashboard $ go generate ./dashboard
``` ```

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,68 @@
// 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 PropTypes from 'prop-types';
import withStyles from 'material-ui/styles/withStyles';
import SideBar from './SideBar.jsx';
import Content from "./Content.jsx";
// Styles for the Body component.
const styles = theme => ({
body: {
display: 'flex',
width: '100%',
height: '100%',
},
});
// Body renders the body of the dashboard.
@withStyles(styles)
class Body extends Component {
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}
/>
<Content
active={this.props.active}
memory={this.props.memory}
traffic={this.props.traffic}
logs={this.props.logs}
shouldUpdate={this.props.shouldUpdate}
/>
</div>
);
}
}
Body.propTypes = {
opened: PropTypes.bool.isRequired,
changeContent: PropTypes.func.isRequired,
active: PropTypes.string.isRequired,
memory: PropTypes.array.isRequired,
traffic: PropTypes.array.isRequired,
logs: PropTypes.array.isRequired,
shouldUpdate: PropTypes.object.isRequired,
};
export default Body;

View file

@ -0,0 +1,48 @@
// 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 PropTypes from 'prop-types';
import Grid from 'material-ui/Grid';
import {ResponsiveContainer} 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>
);
}
}
ChartGrid.propTypes = {
spacing: PropTypes.number.isRequired,
};
export default ChartGrid;

View file

@ -22,15 +22,16 @@ export const LIMIT = {
traffic: 200, // Maximum number of traffic data samples. traffic: 200, // Maximum number of traffic data samples.
log: 200, // Maximum number of logs. log: 200, // Maximum number of logs.
}; };
// The sidebar menu and the main content are rendered based on these elements. // The sidebar menu and the main content are rendered based on these elements.
export const TAGS = (() => { export const TAGS = (() => {
const T = { const T = {
home: { title: "Home", }, home: { title: "Home", icon: "home", },
chain: { title: "Chain", }, chain: { title: "Chain", icon: "link", },
transactions: { title: "Transactions", }, transactions: { title: "Transactions", icon: "credit-card", },
network: { title: "Network", }, network: { title: "Network", icon: "globe", },
system: { title: "System", }, system: { title: "System", icon: "tachometer", },
logs: { title: "Logs", }, logs: { title: "Logs", icon: "list", },
}; };
// Using the key is circumstantial in some cases, so it is better to insert it also as a value. // 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. // This way the mistyping is prevented.
@ -48,5 +49,4 @@ export const DATA_KEYS = (() => {
return DK; return DK;
})(); })();
// Temporary - taken from Material-UI export const DURATION = 200;
export const DRAWER_WIDTH = 240;

View file

@ -0,0 +1,71 @@
// 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 PropTypes from 'prop-types';
import withStyles from 'material-ui/styles/withStyles';
import Home from './Home.jsx';
import {TAGS} from './Common.jsx';
// Styles for the Content component.
const styles = theme => ({
content: {
flexGrow: 1,
backgroundColor: theme.palette.background.default,
padding: theme.spacing.unit * 3,
overflow: 'auto',
},
});
// Content renders the chosen content.
@withStyles(styles)
class Content extends Component {
render() {
const {classes, active, memory, traffic, logs, shouldUpdate} = this.props;
let content = null;
switch(active) {
case TAGS.home.id:
content = <Home memory={memory} traffic={traffic} shouldUpdate={shouldUpdate} />;
break;
case TAGS.chain.id:
content = <div>Chain is under construction.</div>;
break;
case TAGS.transactions.id:
content = <div>Transactions is under construction.</div>;
break;
case TAGS.network.id:
content = <div>Network is under construction.</div>;
break;
case TAGS.system.id:
content = <div>System is under construction.</div>;
break;
case TAGS.logs.id:
content = <div>{logs.map((log, index) => <div key={index}>{log}</div>)}</div>;
}
return <div className={classes.content}>{content}</div>;
}
}
Content.propTypes = {
active: PropTypes.string.isRequired,
shouldUpdate: PropTypes.object.isRequired,
};
export default Content;

View file

@ -15,27 +15,29 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withStyles} from 'material-ui/styles';
import SideBar from './SideBar.jsx'; import withStyles from 'material-ui/styles/withStyles';
import Header from './Header.jsx'; import Header from './Header.jsx';
import Main from "./Main.jsx"; import Body from './Body.jsx';
import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS,} from "./Common.jsx"; import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS} from "./Common.jsx";
// Styles for the Dashboard component. // Styles for the Dashboard component.
const styles = theme => ({ const styles = theme => ({
appFrame: { dashboard: {
position: 'relative',
display: 'flex', display: 'flex',
flexFlow: 'column',
width: '100%', width: '100%',
height: '100%', height: '100%',
background: theme.palette.background.default, 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. // 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. // When there is an incoming message, updates the page's content correspondingly.
@withStyles(styles)
class Dashboard extends Component { class Dashboard extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -45,7 +47,7 @@ class Dashboard extends Component {
memory: [], memory: [],
traffic: [], traffic: [],
logs: [], logs: [],
shouldUpdate: {}, shouldUpdate: {}, // contains the labels of the incoming sample types
}; };
} }
@ -74,7 +76,6 @@ class Dashboard extends Component {
// update analyzes the incoming message, and updates the charts' content correspondingly. // update analyzes the incoming message, and updates the charts' content correspondingly.
update = msg => { update = msg => {
console.log(msg);
this.setState(prevState => { this.setState(prevState => {
let newState = []; let newState = [];
newState.shouldUpdate = {}; newState.shouldUpdate = {};
@ -121,9 +122,9 @@ class Dashboard extends Component {
}); });
}; };
// The change of the active label on the SideBar component will trigger a new render in the Main component. // changeContent sets the active label, which is used at the content rendering.
changeContent = active => { changeContent = newActive => {
this.setState(prevState => prevState.active !== active ? {active: active} : {}); this.setState(prevState => prevState.active !== newActive ? {active: newActive} : {});
}; };
openSideBar = () => { openSideBar = () => {
@ -135,22 +136,18 @@ class Dashboard extends Component {
}; };
render() { render() {
// The classes property is injected by withStyles(). const {classes} = this.props; // The classes property is injected by withStyles().
const {classes} = this.props;
return ( return (
<div className={classes.appFrame}> <div className={classes.dashboard}>
<Header <Header
opened={this.state.sideBar} opened={this.state.sideBar}
open={this.openSideBar} openSideBar={this.openSideBar}
closeSideBar={this.closeSideBar}
/> />
<SideBar <Body
opened={this.state.sideBar} opened={this.state.sideBar}
close={this.closeSideBar}
changeContent={this.changeContent} changeContent={this.changeContent}
/>
<Main
opened={this.state.sideBar}
active={this.state.active} active={this.state.active}
memory={this.state.memory} memory={this.state.memory}
traffic={this.state.traffic} traffic={this.state.traffic}
@ -162,8 +159,4 @@ class Dashboard extends Component {
} }
} }
Dashboard.propTypes = { export default Dashboard;
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Dashboard);

View file

@ -16,61 +16,76 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; 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 AppBar from 'material-ui/AppBar';
import Toolbar from 'material-ui/Toolbar'; import Toolbar from "material-ui/Toolbar";
import Transition from 'react-transition-group/Transition';
import IconButton from "material-ui/IconButton";
import Typography from 'material-ui/Typography'; import Typography from 'material-ui/Typography';
import IconButton from 'material-ui/IconButton'; import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
import MenuIcon from 'material-ui-icons/Menu';
import {DRAWER_WIDTH} from './Common.jsx'; import {DURATION} from './Common.jsx';
// 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. // Styles for the Header component.
const styles = theme => ({ const styles = theme => ({
appBar: { header: {
position: 'absolute', backgroundColor: theme.palette.background.appBar,
transition: theme.transitions.create(['margin', 'width'], { color: theme.palette.getContrastText(theme.palette.background.appBar),
easing: theme.transitions.easing.sharp, zIndex: theme.zIndex.appBar,
duration: theme.transitions.duration.leavingScreen,
}),
}, },
appBarShift: { toolbar: {
marginLeft: DRAWER_WIDTH, paddingLeft: theme.spacing.unit,
width: `calc(100% - ${DRAWER_WIDTH}px)`, paddingRight: theme.spacing.unit,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}, },
menuButton: { mainText: {
marginLeft: 12, paddingLeft: theme.spacing.unit,
marginRight: 20,
},
hide: {
display: 'none',
}, },
}); });
// Header renders a header, which contains a sidebar opener icon when that is closed. // Header renders the header of the dashboard.
@withStyles(styles)
class Header extends Component { class Header extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.opened !== this.props.opened;
}
// changeSideBar opens or closes the sidebar corresponding to the previous state.
changeSideBar = () => {
this.props.opened ? this.props.closeSideBar() : this.props.openSideBar();
};
// arrowButton is connected to the sidebar; changes its state.
arrowButton = transitionState => (
<IconButton onClick={this.changeSideBar}>
<ChevronLeftIcon
style={{
...arrowDefault,
...arrowTransition[transitionState],
}}
/>
</IconButton>
);
render() { render() {
// The classes property is injected by withStyles(). const {classes, opened} = this.props; // The classes property is injected by withStyles().
const {classes} = this.props;
return ( return (
<AppBar className={classNames(classes.appBar, this.props.opened && classes.appBarShift)}> <AppBar position="static" className={classes.header}>
<Toolbar disableGutters={!this.props.opened}> <Toolbar className={classes.toolbar}>
<IconButton <Transition mountOnEnter in={opened} timeout={{enter: DURATION}}>
color="contrast" {this.arrowButton}
aria-label="open drawer" </Transition>
onClick={this.props.open} <Typography type="title" color="inherit" noWrap className={classes.mainText}>
className={classNames(classes.menuButton, this.props.opened && classes.hide)} PoC Go Ethereum Dashboard
>
<MenuIcon />
</IconButton>
<Typography type="title" color="inherit" noWrap>
Go Ethereum Dashboard
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
@ -79,9 +94,9 @@ class Header extends Component {
} }
Header.propTypes = { Header.propTypes = {
classes: PropTypes.object.isRequired,
opened: PropTypes.bool.isRequired, opened: PropTypes.bool.isRequired,
open: PropTypes.func.isRequired, openSideBar: PropTypes.func.isRequired,
closeSideBar: PropTypes.func.isRequired,
}; };
export default withStyles(styles)(Header); export default Header;

View file

@ -16,65 +16,48 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; 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 withTheme from 'material-ui/styles/withTheme';
import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line} from 'recharts';
import ChartGrid from './ChartGrid.jsx';
import {isNullOrUndefined, DATA_KEYS} from "./Common.jsx"; import {isNullOrUndefined, DATA_KEYS} from "./Common.jsx";
// ChartGrid renders a grid container for responsive charts. // Home renders the home content.
// The children are Recharts components extended with the Material-UI's xs property. @withTheme()
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>
);
}
}
ChartGrid.propTypes = {
spacing: PropTypes.number.isRequired,
};
// Home renders the home component.
class Home extends Component { class Home extends Component {
constructor(props) {
super(props);
const {theme} = props; // The theme property is injected by withTheme().
this.memoryColor = theme.palette.primary[300];
this.trafficColor = theme.palette.secondary[300];
}
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.memory]) || return !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.memory]) ||
!isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.traffic]); !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.traffic]);
} }
render() { render() {
const {theme} = this.props; const {memory, traffic} = this.props;
const memoryColor = theme.palette.primary[300];
const trafficColor = theme.palette.secondary[300];
return ( return (
<ChartGrid spacing={24}> <ChartGrid spacing={24}>
<AreaChart xs={6} height={300} values={this.props.memory}> <AreaChart xs={6} height={300} values={memory}>
<YAxis /> <YAxis />
<Area type="monotone" dataKey="value" stroke={memoryColor} fill={memoryColor} /> <Area type="monotone" dataKey="value" stroke={this.memoryColor} fill={this.memoryColor} />
</AreaChart> </AreaChart>
<LineChart xs={6} height={300} values={this.props.traffic}> <LineChart xs={6} height={300} values={traffic}>
<Line type="monotone" dataKey="value" stroke={trafficColor} dot={false} /> <Line type="monotone" dataKey="value" stroke={this.trafficColor} dot={false} />
</LineChart> </LineChart>
<LineChart xs={6} height={300} values={this.props.memory}> <LineChart xs={6} height={300} values={memory}>
<YAxis /> <YAxis />
<CartesianGrid stroke="#eee" strokeDasharray="5 5" /> <CartesianGrid stroke="#eee" strokeDasharray="5 5" />
<Line type="monotone" dataKey="value" stroke={memoryColor} dot={false} /> <Line type="monotone" dataKey="value" stroke={this.memoryColor} dot={false} />
</LineChart> </LineChart>
<AreaChart xs={6} height={300} values={this.props.traffic}> <AreaChart xs={6} height={300} values={traffic}>
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} /> <CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
<Area type="monotone" dataKey="value" stroke={trafficColor} fill={trafficColor} /> <Area type="monotone" dataKey="value" stroke={this.trafficColor} fill={this.trafficColor} />
</AreaChart> </AreaChart>
</ChartGrid> </ChartGrid>
); );
@ -82,8 +65,7 @@ class Home extends Component {
} }
Home.propTypes = { Home.propTypes = {
theme: PropTypes.object.isRequired,
shouldUpdate: PropTypes.object.isRequired, shouldUpdate: PropTypes.object.isRequired,
}; };
export default withTheme()(Home); export default Home;

View file

@ -1,109 +0,0 @@
// 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 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;
}
}
ContentSwitch.propTypes = {
active: PropTypes.string.isRequired,
shouldUpdate: PropTypes.object.isRequired,
};
// styles contains the styles for the Main 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,
}),
},
});
// Main renders a component for the page content.
class Main extends Component {
render() {
// The classes property is injected by withStyles().
const {classes} = this.props;
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>
);
}
}
Main.propTypes = {
classes: PropTypes.object.isRequired,
opened: PropTypes.bool.isRequired,
active: PropTypes.string.isRequired,
shouldUpdate: PropTypes.object.isRequired,
};
export default withStyles(styles)(Main);

View file

@ -16,35 +16,38 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import PropTypes from 'prop-types'; 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 {TAGS, DURATION} from './Common.jsx';
// menuDefault is the default style of the menu.
const menuDefault = {
transition: `margin-left ${DURATION}ms`,
};
// menu Transition is the additional style of the menu corresponding to the transition's state.
const menuTransition = {
entered: {marginLeft: -200},
};
// Styles for the SideBar component. // Styles for the SideBar component.
const styles = theme => ({ const styles = theme => ({
drawerPaper: { list: {
position: 'relative', background: theme.palette.background.appBar,
height: '100%',
width: DRAWER_WIDTH,
}, },
drawerHeader: { listItem: {
display: 'flex', minWidth: theme.spacing.unit * 3,
alignItems: 'center', },
justifyContent: 'flex-end', icon: {
padding: '0 8px', fontSize: theme.spacing.unit * 3,
...theme.mixins.toolbar,
transitionDuration: {
enter: theme.transitions.duration.enteringScreen,
exit: theme.transitions.duration.leavingScreen,
}
}, },
}); });
// SideBar renders a sidebar component. // SideBar renders the sidebar of the dashboard.
@withStyles(styles)
class SideBar extends Component { class SideBar extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -57,50 +60,58 @@ class SideBar extends Component {
const id = TAGS[key].id; const id = TAGS[key].id;
this.clickOn[id] = event => { this.clickOn[id] = event => {
event.preventDefault(); event.preventDefault();
console.log(event.target.key); props.changeContent(id);
this.props.changeContent(id);
}; };
} }
} }
render() { shouldComponentUpdate(nextProps) {
// The classes property is injected by withStyles(). return nextProps.opened !== this.props.opened;
const {classes} = this.props; }
// menu renders the list of the menu items.
menu = transitionState => {
const {classes} = this.props; // The classes property is injected by withStyles().
return ( return (
<Drawer <div className={classes.list}>
type="persistent"
classes={{paper: classes.drawerPaper,}}
open={this.props.opened}
>
<div>
<div className={classes.drawerHeader}>
<IconButton onClick={this.props.close}>
<ChevronLeftIcon />
</IconButton>
</div>
<List> <List>
{ {
Object.values(TAGS).map(tag => { Object.values(TAGS).map(tag => (
return ( <ListItem button key={tag.id} onClick={this.clickOn[tag.id]} className={classes.listItem}>
<ListItem button key={tag.id} onClick={this.clickOn[tag.id]}> <ListItemIcon>
<ListItemText primary={tag.title} /> <Icon className={classes.icon}>
<FontAwesome name={tag.icon} />
</Icon>
</ListItemIcon>
<ListItemText
primary={tag.title}
style={{
...menuDefault,
...menuTransition[transitionState],
padding: 0,
}}
/>
</ListItem> </ListItem>
); ))
})
} }
</List> </List>
</div> </div>
</Drawer> );
};
render() {
return (
<Transition mountOnEnter in={this.props.opened} timeout={{enter: DURATION}}>
{this.menu}
</Transition>
); );
} }
} }
SideBar.propTypes = { SideBar.propTypes = {
classes: PropTypes.object.isRequired,
opened: PropTypes.bool.isRequired, opened: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
changeContent: PropTypes.func.isRequired, changeContent: PropTypes.func.isRequired,
}; };
export default withStyles(styles)(SideBar); export default SideBar;

View file

@ -0,0 +1,25 @@
// 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/>.
// faOnlyWoffLoader removes the .eot, .ttf, .svg dependencies of the FontAwesome, because they produce unused extra blobs.
module.exports = function(content) {
content = content.replace(/src.*url(?!.*url.*(\.eot)).*(\.eot)[^;]*;/,'');
content = content.replace(/url(?!.*url.*(\.eot)).*(\.eot)[^,]*,/,'');
content = content.replace(/url(?!.*url.*(\.ttf)).*(\.ttf)[^,]*,/,'');
content = content.replace(/,[^,]*url(?!.*url.*(\.svg)).*(\.svg)[^;]*;/,';');
return content;
};

View file

@ -15,7 +15,8 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'; import React from 'react';
import {hydrate} from 'react-dom'; import {render} from 'react-dom';
import {createMuiTheme, MuiThemeProvider} from 'material-ui/styles'; import {createMuiTheme, MuiThemeProvider} from 'material-ui/styles';
import Dashboard from './components/Dashboard.jsx'; import Dashboard from './components/Dashboard.jsx';
@ -28,7 +29,7 @@ const theme = createMuiTheme({
}); });
// Renders the whole dashboard. // Renders the whole dashboard.
hydrate( render(
<MuiThemeProvider theme={theme}> <MuiThemeProvider theme={theme}>
<Dashboard /> <Dashboard />
</MuiThemeProvider>, </MuiThemeProvider>,

View file

@ -1,22 +1,30 @@
{ {
"dependencies": { "dependencies": {
"material-ui": "^1.0.0-beta.21",
"material-ui-icons": "^1.0.0-beta.17",
"react-fa": "^5.0.0",
"react-transition-group": "^2.2.1",
"recharts": "^1.0.0-beta.1",
"classnames": "^2.2.5",
"eslint": "^4.11.0",
"eslint-plugin-react": "^7.5.1",
"prop-types": "^15.6.0",
"react": "^16.1.1",
"react-dom": "^16.1.1",
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-eslint": "^8.0.1", "babel-eslint": "^8.0.2",
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1", "babel-preset-stage-0": "^6.24.1",
"classnames": "^2.2.5", "css-loader": "^0.28.7",
"eslint": "^4.5.0",
"eslint-plugin-react": "^7.4.0",
"material-ui": "^1.0.0-beta.18",
"material-ui-icons": "^1.0.0-beta.17",
"path": "^0.12.7", "path": "^0.12.7",
"prop-types": "^15.6.0", "style-loader": "^0.19.0",
"recharts": "^1.0.0-beta.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"url": "^0.11.0", "url": "^0.11.0",
"url-loader": "^0.6.2",
"webpack": "^3.5.5" "webpack": "^3.5.5"
} }
} }

View file

@ -7,8 +7,14 @@
<title>Go Ethereum Dashboard</title> <title>Go Ethereum Dashboard</title>
<link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico" /> <link rel="shortcut icon" type="image/ico" href="https://ethereum.org/favicon.ico" />
<style>
<!-- TODO (kurkomisi): Return to the external libraries to speed up the bundling during development --> ::-webkit-scrollbar {
width: 16px;
}
::-webkit-scrollbar-thumb {
background: #212121;
}
</style>
</head> </head>
<body style="height: 100%; margin: 0"> <body style="height: 100%; margin: 0">
<div id="dashboard" style="height: 100%"></div> <div id="dashboard" style="height: 100%"></div>

View file

@ -14,6 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
const webpack = require('webpack');
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
@ -22,14 +23,34 @@ module.exports = {
path: path.resolve(__dirname, 'public'), path: path.resolve(__dirname, 'public'),
filename: 'bundle.js', filename: 'bundle.js',
}, },
plugins: [
new webpack.optimize.UglifyJsPlugin({
comments: false,
mangle: false,
beautify: true,
}),
],
module: { module: {
loaders: [ loaders: [
{ {
test: /\.jsx$/, // regexp for JSX files test: /\.jsx$/, // regexp for JSX files
loader: 'babel-loader', // The babel configuration is in the package.json. loader: 'babel-loader',
query: { query: {
presets: ['env', 'react', 'stage-0'] plugins: ['transform-decorators-legacy'], // @withStyles, @withTheme
} presets: ['env', 'react', 'stage-0'],
},
},
{
test: /font-awesome\.css$/,
use: [
'style-loader',
'css-loader',
path.resolve(__dirname, './faOnlyWoffLoader.js'),
],
},
{
test: /\.woff2?$/,
loader: 'url-loader',
}, },
], ],
}, },

View file

@ -16,7 +16,9 @@
package dashboard package dashboard
//go:generate go-bindata -nometadata -o assets.go -prefix assets -pkg dashboard assets/public/... //go:generate ./assets/node_modules/.bin/webpack --config ./assets/webpack.config.js --context ./assets
//go:generate gofmt -s -w .
//go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/public/...
import ( import (
"fmt" "fmt"
@ -277,6 +279,7 @@ func (db *Dashboard) collectData() {
func (db *Dashboard) collectLogs() { func (db *Dashboard) collectLogs() {
defer db.wg.Done() defer db.wg.Done()
id := 1
// TODO (kurkomisi): log collection comes here. // TODO (kurkomisi): log collection comes here.
for { for {
select { select {
@ -285,8 +288,9 @@ func (db *Dashboard) collectLogs() {
return return
case <-time.After(db.config.Refresh / 2): case <-time.After(db.config.Refresh / 2):
db.sendToAll(&message{ db.sendToAll(&message{
Log: "This is a fake log.", Log: fmt.Sprint(id, ": This is a fake log."),
}) })
id++
} }
} }
} }