cmd, dashboard: use webpack dev server, remove custom assets (#16263)

* cmd, dashboard: remove custom assets, webpack dev server

* dashboard: yarn commands, small fixes
This commit is contained in:
Kurkó Mihály 2018-03-08 10:22:21 +02:00 committed by Péter Szilágyi
parent 3ec1b9a92d
commit 704840a8ad
20 changed files with 8900 additions and 10024 deletions

3
.gitignore vendored

@ -42,3 +42,6 @@ profile.cov
/dashboard/assets/node_modules /dashboard/assets/node_modules
/dashboard/assets/stats.json /dashboard/assets/stats.json
/dashboard/assets/bundle.js /dashboard/assets/bundle.js
/dashboard/assets/package-lock.json
**/yarn-error.log

@ -65,7 +65,6 @@ var (
utils.DashboardAddrFlag, utils.DashboardAddrFlag,
utils.DashboardPortFlag, utils.DashboardPortFlag,
utils.DashboardRefreshFlag, utils.DashboardRefreshFlag,
utils.DashboardAssetsFlag,
utils.EthashCacheDirFlag, utils.EthashCacheDirFlag,
utils.EthashCachesInMemoryFlag, utils.EthashCachesInMemoryFlag,
utils.EthashCachesOnDiskFlag, utils.EthashCachesOnDiskFlag,

@ -209,11 +209,6 @@ var (
Usage: "Dashboard metrics collection refresh rate", Usage: "Dashboard metrics collection refresh rate",
Value: dashboard.DefaultConfig.Refresh, Value: dashboard.DefaultConfig.Refresh,
} }
DashboardAssetsFlag = cli.StringFlag{
Name: "dashboard.assets",
Usage: "Developer flag to serve the dashboard from the local file system",
Value: dashboard.DefaultConfig.Assets,
}
// Ethash settings // Ethash settings
EthashCacheDirFlag = DirectoryFlag{ EthashCacheDirFlag = DirectoryFlag{
Name: "ethash.cachedir", Name: "ethash.cachedir",
@ -1120,7 +1115,6 @@ func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name) cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name) cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name) cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
cfg.Assets = ctx.GlobalString(DashboardAssetsFlag.Name)
} }
// RegisterEthService adds an Ethereum client to the stack. // RegisterEthService adds an Ethereum client to the stack.

@ -12,28 +12,27 @@ The client's UI uses [React][React] with JSX syntax, which is validated by the [
As the dashboard depends on certain NPM packages (which are not included in the `go-ethereum` repo), these need to be installed first: As the dashboard depends on certain NPM packages (which are not included in the `go-ethereum` repo), these need to be installed first:
``` ```
$ (cd dashboard/assets && npm install) $ (cd dashboard/assets && yarn install && yarn flow)
$ (cd dashboard/assets && ./node_modules/.bin/flow-typed install)
``` ```
Normally the dashboard assets are bundled into Geth via `go-bindata` to avoid external dependencies. Rebuilding Geth after each UI modification however is not feasible from a developer perspective. Instead, we can run `webpack` in watch mode to automatically rebundle the UI, and ask `geth` to use external assets to not rely on compiled resources: Normally the dashboard assets are bundled into Geth via `go-bindata` to avoid external dependencies. Rebuilding Geth after each UI modification however is not feasible from a developer perspective. Instead, we can run `yarn dev` to watch for file system changes and refresh the browser automatically.
``` ```
$ (cd dashboard/assets && ./node_modules/.bin/webpack --watch) $ geth --dashboard --vmodule=dashboard=5
$ geth --dashboard --dashboard.assets=dashboard/assets --vmodule=dashboard=5 $ (cd dashboard/assets && yarn dev)
``` ```
To bundle up the final UI into Geth, run `go generate`: To bundle up the final UI into Geth, run `go generate`:
``` ```
$ go generate ./dashboard $ (cd dashboard && go generate)
``` ```
### Static type checking ### Static type checking
Since JavaScript doesn't provide type safety, [Flow][Flow] is used to check types. These are only useful during development, so at the end of the process Babel will strip them. Since JavaScript doesn't provide type safety, [Flow][Flow] is used to check types. These are only useful during development, so at the end of the process Babel will strip them.
To take advantage of static type checking, your IDE needs to be prepared for it. In case of [Atom][Atom] a configuration guide can be found [here][Atom config]: Install the [Nuclide][Nuclide] package for Flow support, making sure it installs all of its support packages by enabling `Install Recommended Packages on Startup`, and set the path of the `flow-bin` which were installed previously by `npm`. To take advantage of static type checking, your IDE needs to be prepared for it. In case of [Atom][Atom] a configuration guide can be found [here][Atom config]: Install the [Nuclide][Nuclide] package for Flow support, making sure it installs all of its support packages by enabling `Install Recommended Packages on Startup`, and set the path of the `flow-bin` which were installed previously by `yarn`.
For more IDE support install the `linter-eslint` package too, which finds the `.eslintrc` file, and provides real-time linting. Atom warns, that these two packages are incompatible, but they seem to work well together. For third-party library errors and auto-completion [flow-typed][flow-typed] is used. For more IDE support install the `linter-eslint` package too, which finds the `.eslintrc` file, and provides real-time linting. Atom warns, that these two packages are incompatible, but they seem to work well together. For third-party library errors and auto-completion [flow-typed][flow-typed] is used.
@ -41,7 +40,7 @@ For more IDE support install the `linter-eslint` package too, which finds the `.
[Webpack][Webpack] offers handy tools for visualizing the bundle's dependency tree and space usage. [Webpack][Webpack] offers handy tools for visualizing the bundle's dependency tree and space usage.
* Generate the bundle's profile running `webpack --profile --json > stats.json` * Generate the bundle's profile running `yarn stats`
* For the _dependency tree_ go to [Webpack Analyze][WA], and import `stats.json` * For the _dependency tree_ go to [Webpack Analyze][WA], and import `stats.json`
* For the _space usage_ go to [Webpack Visualizer][WV], and import `stats.json` * For the _space usage_ go to [Webpack Visualizer][WV], and import `stats.json`

File diff suppressed because one or more lines are too long

@ -38,15 +38,15 @@ export const percentPlotter = <T>(text: string, mapper: (T => T) = multiplier(1)
}; };
// unit contains the units for the bytePlotter. // unit contains the units for the bytePlotter.
const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; const unit = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
// simplifyBytes returns the simplified version of the given value followed by the unit. // simplifyBytes returns the simplified version of the given value followed by the unit.
const simplifyBytes = (x: number) => { const simplifyBytes = (x: number) => {
let i = 0; let i = 0;
for (; x > 1024 && i < 5; i++) { for (; x > 1024 && i < 8; i++) {
x /= 1024; x /= 1024;
} }
return x.toFixed(2).toString().concat(' ', unit[i]); return x.toFixed(2).toString().concat(' ', unit[i], 'B');
}; };
// bytePlotter renders a tooltip, which displays the payload as a byte value. // bytePlotter renders a tooltip, which displays the payload as a byte value.

@ -81,7 +81,11 @@ const defaultContent: Content = {
version: null, version: null,
commit: null, commit: null,
}, },
home: { home: {},
chain: {},
txpool: {},
network: {},
system: {
activeMemory: [], activeMemory: [],
virtualMemory: [], virtualMemory: [],
networkIngress: [], networkIngress: [],
@ -91,10 +95,6 @@ const defaultContent: Content = {
diskRead: [], diskRead: [],
diskWrite: [], diskWrite: [],
}, },
chain: {},
txpool: {},
network: {},
system: {},
logs: { logs: {
log: [], log: [],
}, },
@ -108,7 +108,11 @@ const updaters = {
version: replacer, version: replacer,
commit: replacer, commit: replacer,
}, },
home: { home: null,
chain: null,
txpool: null,
network: null,
system: {
activeMemory: appender(200), activeMemory: appender(200),
virtualMemory: appender(200), virtualMemory: appender(200),
networkIngress: appender(200), networkIngress: appender(200),
@ -118,10 +122,6 @@ const updaters = {
diskRead: appender(200), diskRead: appender(200),
diskWrite: appender(200), diskWrite: appender(200),
}, },
chain: null,
txpool: null,
network: null,
system: null,
logs: { logs: {
log: appender(200), log: appender(200),
}, },
@ -136,7 +136,7 @@ const styles = {
height: '100%', height: '100%',
zIndex: 1, zIndex: 1,
overflow: 'hidden', overflow: 'hidden',
} },
}; };
// themeStyles returns the styles generated from the theme for the component. // themeStyles returns the styles generated from the theme for the component.
@ -178,7 +178,8 @@ class Dashboard extends Component<Props, State> {
// reconnect establishes a websocket connection with the server, listens for incoming messages // reconnect establishes a websocket connection with the server, listens for incoming messages
// and tries to reconnect on connection loss. // and tries to reconnect on connection loss.
reconnect = () => { reconnect = () => {
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`); // PROD is defined by webpack.
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${PROD ? window.location.host : 'localhost:8080'}/api`);
server.onopen = () => { server.onopen = () => {
this.setState({content: defaultContent, shouldUpdate: {}}); this.setState({content: defaultContent, shouldUpdate: {}});
}; };
@ -217,7 +218,6 @@ class Dashboard extends Component<Props, State> {
return ( return (
<div className={this.props.classes.dashboard} style={styles.dashboard}> <div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header <Header
opened={this.state.sideBar}
switchSideBar={this.switchSideBar} switchSideBar={this.switchSideBar}
/> />
<Body <Body

@ -26,7 +26,17 @@ import {ResponsiveContainer, AreaChart, Area, Tooltip} from 'recharts';
import ChartRow from './ChartRow'; import ChartRow from './ChartRow';
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from './CustomTooltip'; import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from './CustomTooltip';
import {styles as commonStyles} from '../common'; import {styles as commonStyles} from '../common';
import type {Content} from '../types/content'; import type {General, System} from '../types/content';
const FOOTER_SYNC_ID = 'footerSyncId';
const CPU = 'cpu';
const MEMORY = 'memory';
const DISK = 'disk';
const TRAFFIC = 'traffic';
const TOP = 'Top';
const BOTTOM = 'Bottom';
// styles contains the constant styles of the component. // styles contains the constant styles of the component.
const styles = { const styles = {
@ -42,15 +52,14 @@ const styles = {
doubleChartWrapper: { doubleChartWrapper: {
height: '100%', height: '100%',
width: '99%', width: '99%',
paddingTop: 5,
}, },
}; };
// themeStyles returns the styles generated from the theme for the component. // themeStyles returns the styles generated from the theme for the component.
const themeStyles: Object = (theme: Object) => ({ const themeStyles: Object = (theme: Object) => ({
footer: { footer: {
backgroundColor: theme.palette.background.appBar, backgroundColor: theme.palette.grey[900],
color: theme.palette.getContrastText(theme.palette.background.appBar), color: theme.palette.getContrastText(theme.palette.grey[900]),
zIndex: theme.zIndex.appBar, zIndex: theme.zIndex.appBar,
height: theme.spacing.unit * 10, height: theme.spacing.unit * 10,
}, },
@ -59,111 +68,108 @@ const themeStyles: Object = (theme: Object) => ({
export type Props = { export type Props = {
classes: Object, // injected by withStyles() classes: Object, // injected by withStyles()
theme: Object, theme: Object,
content: Content, general: General,
system: System,
shouldUpdate: Object, shouldUpdate: Object,
}; };
// Footer renders the footer of the dashboard. // Footer renders the footer of the dashboard.
class Footer extends Component<Props> { class Footer extends Component<Props> {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return typeof nextProps.shouldUpdate.home !== 'undefined'; return typeof nextProps.shouldUpdate.general !== 'undefined' || typeof nextProps.shouldUpdate.system !== 'undefined';
} }
// info renders a label with the given values. // halfHeightChart renders an area chart with half of the height of its parent.
info = (about: string, value: ?string) => (value ? ( halfHeightChart = (chartProps, tooltip, areaProps) => (
<Typography type='caption' color='inherit'> <ResponsiveContainer width='100%' height='50%'>
<span style={commonStyles.light}>{about}</span> {value} <AreaChart {...chartProps} >
</Typography> {!tooltip || (<Tooltip cursor={false} content={<CustomTooltip tooltip={tooltip} />} />)}
) : null); <Area isAnimationActive={false} type='monotone' {...areaProps} />
</AreaChart>
</ResponsiveContainer>
);
// doubleChart renders a pair of charts separated by the baseline. // doubleChart renders a pair of charts separated by the baseline.
doubleChart = (syncId, topChart, bottomChart) => { doubleChart = (syncId, chartKey, topChart, bottomChart) => {
const topKey = 'topKey'; if (!Array.isArray(topChart.data) || !Array.isArray(bottomChart.data)) {
const bottomKey = 'bottomKey'; return null;
const topDefault = topChart.default ? topChart.default : 0; }
const bottomDefault = bottomChart.default ? bottomChart.default : 0; const topDefault = topChart.default || 0;
const topTooltip = topChart.tooltip ? ( const bottomDefault = bottomChart.default || 0;
<Tooltip cursor={false} content={<CustomTooltip tooltip={topChart.tooltip} />} /> const topKey = `${chartKey}${TOP}`;
) : null; const bottomKey = `${chartKey}${BOTTOM}`;
const bottomTooltip = bottomChart.tooltip ? (
<Tooltip cursor={false} content={<CustomTooltip tooltip={bottomChart.tooltip} />} />
) : null;
const topColor = '#8884d8'; const topColor = '#8884d8';
const bottomColor = '#82ca9d'; 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 ( return (
<div style={styles.doubleChartWrapper}> <div style={styles.doubleChartWrapper}>
<ResponsiveContainer width='100%' height='50%'> {this.halfHeightChart(
<AreaChart data={data} syncId={syncId} > {
{topTooltip} syncId,
<Area type='monotone' dataKey={topKey} stroke={topColor} fill={topColor} /> data: topChart.data.map(({value}) => ({[topKey]: value || topDefault})),
</AreaChart> margin: {top: 5, right: 5, bottom: 0, left: 5},
</ResponsiveContainer> },
<div style={{marginTop: -10, width: '100%', height: '50%'}}> topChart.tooltip,
<ResponsiveContainer width='100%' height='100%'> {dataKey: topKey, stroke: topColor, fill: topColor},
<AreaChart data={data} syncId={syncId} > )}
{bottomTooltip} {this.halfHeightChart(
<Area type='monotone' dataKey={bottomKey} stroke={bottomColor} fill={bottomColor} /> {
</AreaChart> syncId,
</ResponsiveContainer> data: bottomChart.data.map(({value}) => ({[bottomKey]: -value || -bottomDefault})),
</div> margin: {top: 0, right: 5, bottom: 5, left: 5},
},
bottomChart.tooltip,
{dataKey: bottomKey, stroke: bottomColor, fill: bottomColor},
)}
</div> </div>
); );
} };
render() { render() {
const {content} = this.props; const {general, system} = this.props;
const {general, home} = content;
return ( return (
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}> <Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
<Grid item xs style={styles.chartRowWrapper}> <Grid item xs style={styles.chartRowWrapper}>
<ChartRow> <ChartRow>
{this.doubleChart( {this.doubleChart(
'all', FOOTER_SYNC_ID,
{data: home.processCPU, tooltip: percentPlotter('Process')}, CPU,
{data: home.systemCPU, tooltip: percentPlotter('System', multiplier(-1))}, {data: system.processCPU, tooltip: percentPlotter('Process load')},
{data: system.systemCPU, tooltip: percentPlotter('System load', multiplier(-1))},
)} )}
{this.doubleChart( {this.doubleChart(
'all', FOOTER_SYNC_ID,
{data: home.activeMemory, tooltip: bytePlotter('Active')}, MEMORY,
{data: home.virtualMemory, tooltip: bytePlotter('Virtual', multiplier(-1))}, {data: system.activeMemory, tooltip: bytePlotter('Active memory')},
{data: system.virtualMemory, tooltip: bytePlotter('Virtual memory', multiplier(-1))},
)} )}
{this.doubleChart( {this.doubleChart(
'all', FOOTER_SYNC_ID,
{data: home.diskRead, tooltip: bytePerSecPlotter('Disk Read')}, DISK,
{data: home.diskWrite, tooltip: bytePerSecPlotter('Disk Write', multiplier(-1))}, {data: system.diskRead, tooltip: bytePerSecPlotter('Disk read')},
{data: system.diskWrite, tooltip: bytePerSecPlotter('Disk write', multiplier(-1))},
)} )}
{this.doubleChart( {this.doubleChart(
'all', FOOTER_SYNC_ID,
{data: home.networkIngress, tooltip: bytePerSecPlotter('Download')}, TRAFFIC,
{data: home.networkEgress, tooltip: bytePerSecPlotter('Upload', multiplier(-1))}, {data: system.networkIngress, tooltip: bytePerSecPlotter('Download')},
{data: system.networkEgress, tooltip: bytePerSecPlotter('Upload', multiplier(-1))},
)} )}
</ChartRow> </ChartRow>
</Grid> </Grid>
<Grid item > <Grid item >
{this.info('Geth', general.version)} <Typography type='caption' color='inherit'>
{this.info('Commit', general.commit ? general.commit.substring(0, 7) : null)} <span style={commonStyles.light}>Geth</span> {general.version}
</Typography>
{general.commit && (
<Typography type='caption' color='inherit'>
<span style={commonStyles.light}>{'Commit '}</span>
<a href={`https://github.com/ethereum/go-ethereum/commit/${general.commit}`} target='_blank' style={{color: 'inherit', textDecoration: 'none'}} >
{general.commit.substring(0, 8)}
</a>
</Typography>
)}
</Grid> </Grid>
</Grid> </Grid>
); );

@ -21,30 +21,16 @@ import React, {Component} from 'react';
import withStyles from 'material-ui/styles/withStyles'; import withStyles from 'material-ui/styles/withStyles';
import AppBar from 'material-ui/AppBar'; import 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 IconButton from 'material-ui/IconButton';
import Icon from 'material-ui/Icon';
import MenuIcon from 'material-ui-icons/Menu';
import Typography from 'material-ui/Typography'; import Typography from 'material-ui/Typography';
import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
import {DURATION} from '../common';
// styles contains the constant styles of the component.
const styles = {
arrow: {
default: {
transition: `transform ${DURATION}ms`,
},
transition: {
entered: {transform: 'rotate(180deg)'},
},
},
};
// themeStyles returns the styles generated from the theme for the component. // themeStyles returns the styles generated from the theme for the component.
const themeStyles = (theme: Object) => ({ const themeStyles = (theme: Object) => ({
header: { header: {
backgroundColor: theme.palette.background.appBar, backgroundColor: theme.palette.grey[900],
color: theme.palette.getContrastText(theme.palette.background.appBar), color: theme.palette.getContrastText(theme.palette.grey[900]),
zIndex: theme.zIndex.appBar, zIndex: theme.zIndex.appBar,
}, },
toolbar: { toolbar: {
@ -53,42 +39,28 @@ const themeStyles = (theme: Object) => ({
}, },
title: { title: {
paddingLeft: theme.spacing.unit, paddingLeft: theme.spacing.unit,
fontSize: 3 * theme.spacing.unit,
}, },
}); });
export type Props = { export type Props = {
classes: Object, // injected by withStyles() classes: Object, // injected by withStyles()
opened: boolean,
switchSideBar: () => void, switchSideBar: () => void,
}; };
// Header renders the header of the dashboard. // Header renders the header of the dashboard.
class Header extends Component<Props> { class Header extends Component<Props> {
shouldComponentUpdate(nextProps) {
return nextProps.opened !== this.props.opened;
}
// arrow renders a button, which changes the sidebar's state.
arrow = (transitionState: string) => (
<IconButton onClick={this.props.switchSideBar}>
<ChevronLeftIcon
style={{
...styles.arrow.default,
...styles.arrow.transition[transitionState],
}}
/>
</IconButton>
);
render() { render() {
const {classes, opened} = this.props; const {classes} = this.props;
return ( return (
<AppBar position='static' className={classes.header}> <AppBar position='static' className={classes.header}>
<Toolbar className={classes.toolbar}> <Toolbar className={classes.toolbar}>
<Transition mountOnEnter in={opened} timeout={{enter: DURATION}}> <IconButton onClick={this.props.switchSideBar}>
{this.arrow} <Icon>
</Transition> <MenuIcon />
</Icon>
</IconButton>
<Typography type='title' color='inherit' noWrap className={classes.title}> <Typography type='title' color='inherit' noWrap className={classes.title}>
Go Ethereum Dashboard Go Ethereum Dashboard
</Typography> </Typography>

@ -76,7 +76,8 @@ class Main extends Component<Props> {
<div style={styles.wrapper}> <div style={styles.wrapper}>
<div className={classes.content} style={styles.content}>{children}</div> <div className={classes.content} style={styles.content}>{children}</div>
<Footer <Footer
content={content} general={content.general}
system={content.system}
shouldUpdate={shouldUpdate} shouldUpdate={shouldUpdate}
/> />
</div> </div>

@ -41,10 +41,10 @@ const styles = {
// themeStyles returns the styles generated from the theme for the component. // themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({ const themeStyles = theme => ({
list: { list: {
background: theme.palette.background.appBar, background: theme.palette.grey[900],
}, },
listItem: { listItem: {
minWidth: theme.spacing.unit * 3, minWidth: theme.spacing.unit * 7,
}, },
icon: { icon: {
fontSize: theme.spacing.unit * 3, fontSize: theme.spacing.unit * 3,

File diff suppressed because it is too large Load Diff

@ -15,11 +15,11 @@
"css-loader": "^0.28.9", "css-loader": "^0.28.9",
"eslint": "^4.16.0", "eslint": "^4.16.0",
"eslint-config-airbnb": "^16.1.0", "eslint-config-airbnb": "^16.1.0",
"eslint-loader": "^1.9.0", "eslint-loader": "^2.0.0",
"eslint-plugin-flowtype": "^2.41.0",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1", "eslint-plugin-react": "^7.5.1",
"eslint-plugin-flowtype": "^2.41.0",
"file-loader": "^1.1.6", "file-loader": "^1.1.6",
"flow-bin": "^0.63.1", "flow-bin": "^0.63.1",
"flow-bin-loader": "^1.0.2", "flow-bin-loader": "^1.0.2",
@ -35,6 +35,13 @@
"style-loader": "^0.19.1", "style-loader": "^0.19.1",
"url": "^0.11.0", "url": "^0.11.0",
"url-loader": "^0.6.2", "url-loader": "^0.6.2",
"webpack": "^3.10.0" "webpack": "^3.10.0",
"webpack-dev-server": "^2.11.1"
},
"scripts": {
"build": "NODE_ENV=production webpack",
"stats": "webpack --profile --json > stats.json",
"dev": "webpack-dev-server --port 8081",
"flow": "flow-typed install"
} }
} }

@ -26,27 +26,20 @@ export type Content = {
logs: Logs, logs: Logs,
}; };
export type ChartEntries = Array<ChartEntry>;
export type ChartEntry = {
time: Date,
value: number,
};
export type General = { export type General = {
version: ?string, version: ?string,
commit: ?string, commit: ?string,
}; };
export type Home = { export type Home = {
activeMemory: ChartEntries, /* TODO (kurkomisi) */
virtualMemory: ChartEntries,
networkIngress: ChartEntries,
networkEgress: ChartEntries,
processCPU: ChartEntries,
systemCPU: ChartEntries,
diskRead: ChartEntries,
diskWrite: ChartEntries,
};
export type ChartEntries = Array<ChartEntry>;
export type ChartEntry = {
time: Date,
value: number,
}; };
export type Chain = { export type Chain = {
@ -62,7 +55,14 @@ export type Network = {
}; };
export type System = { export type System = {
/* TODO (kurkomisi) */ activeMemory: ChartEntries,
virtualMemory: ChartEntries,
networkIngress: ChartEntries,
networkEgress: ChartEntries,
processCPU: ChartEntries,
systemCPU: ChartEntries,
diskRead: ChartEntries,
diskWrite: ChartEntries,
}; };
export type Logs = { export type Logs = {

@ -32,6 +32,9 @@ module.exports = {
mangle: false, mangle: false,
beautify: true, beautify: true,
}), }),
new webpack.DefinePlugin({
PROD: process.env.NODE_ENV === 'production',
}),
], ],
module: { module: {
rules: [ rules: [

6551
dashboard/assets/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

@ -38,8 +38,4 @@ type Config struct {
// Refresh is the refresh rate of the data updates, the chartEntry will be collected this often. // Refresh is the refresh rate of the data updates, the chartEntry will be collected this often.
Refresh time.Duration `toml:",omitempty"` Refresh time.Duration `toml:",omitempty"`
// Assets offers a possibility to manually set the dashboard website's location on the server side.
// It is useful for debugging, avoids the repeated generation of the binary.
Assets string `toml:",omitempty"`
} }

@ -16,19 +16,17 @@
package dashboard package dashboard
//go:generate npm --prefix ./assets install //go:generate yarn --cwd ./assets install
//go:generate ./assets/node_modules/.bin/webpack --config ./assets/webpack.config.js --context ./assets //go:generate yarn --cwd ./assets build
//go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/dashboard.html assets/bundle.js //go:generate go-bindata -nometadata -o assets.go -prefix assets -nocompress -pkg dashboard assets/index.html assets/bundle.js
//go:generate sh -c "sed 's#var _bundleJs#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go" //go:generate sh -c "sed 's#var _bundleJs#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
//go:generate sh -c "sed 's#var _dashboardHtml#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go" //go:generate sh -c "sed 's#var _indexHtml#//nolint:misspell\\\n&#' assets.go > assets.go.tmp && mv assets.go.tmp assets.go"
//go:generate gofmt -w -s assets.go //go:generate gofmt -w -s assets.go
import ( import (
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"path/filepath"
"runtime" "runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -62,7 +60,7 @@ type Dashboard struct {
listener net.Listener listener net.Listener
conns map[uint32]*client // Currently live websocket connections conns map[uint32]*client // Currently live websocket connections
charts *HomeMessage charts *SystemMessage
commit string commit string
lock sync.RWMutex // Lock protecting the dashboard's internals lock sync.RWMutex // Lock protecting the dashboard's internals
@ -84,7 +82,7 @@ func New(config *Config, commit string) (*Dashboard, error) {
conns: make(map[uint32]*client), conns: make(map[uint32]*client),
config: config, config: config,
quit: make(chan chan error), quit: make(chan chan error),
charts: &HomeMessage{ charts: &SystemMessage{
ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh), ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh),
VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh), VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh),
NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh), NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh),
@ -180,18 +178,7 @@ func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
path := r.URL.String() path := r.URL.String()
if path == "/" { if path == "/" {
path = "/dashboard.html" path = "/index.html"
}
// If the path of the assets is manually set
if db.config.Assets != "" {
blob, err := ioutil.ReadFile(filepath.Join(db.config.Assets, path))
if err != nil {
log.Warn("Failed to read file", "path", path, "err", err)
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Write(blob)
return
} }
blob, err := Asset(path[1:]) blob, err := Asset(path[1:])
if err != nil { if err != nil {
@ -241,7 +228,7 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta), Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
Commit: db.commit, Commit: db.commit,
}, },
Home: &HomeMessage{ System: &SystemMessage{
ActiveMemory: db.charts.ActiveMemory, ActiveMemory: db.charts.ActiveMemory,
VirtualMemory: db.charts.VirtualMemory, VirtualMemory: db.charts.VirtualMemory,
NetworkIngress: db.charts.NetworkIngress, NetworkIngress: db.charts.NetworkIngress,
@ -277,6 +264,8 @@ func (db *Dashboard) collectData() {
systemCPUUsage := gosigar.Cpu{} systemCPUUsage := gosigar.Cpu{}
systemCPUUsage.Get() systemCPUUsage.Get()
var ( var (
mem runtime.MemStats
prevNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count() prevNetworkIngress = metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Count()
prevNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count() prevNetworkEgress = metrics.DefaultRegistry.Get("p2p/OutboundTraffic").(metrics.Meter).Count()
prevProcessCPUTime = getProcessCPUTime() prevProcessCPUTime = getProcessCPUTime()
@ -306,7 +295,7 @@ func (db *Dashboard) collectData() {
deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress) deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress)
deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress) deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress)
deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime
deltaSystemCPUUsage = systemCPUUsage.Delta(prevSystemCPUUsage) deltaSystemCPUUsage = curSystemCPUUsage.Delta(prevSystemCPUUsage)
deltaDiskRead = curDiskRead - prevDiskRead deltaDiskRead = curDiskRead - prevDiskRead
deltaDiskWrite = curDiskWrite - prevDiskWrite deltaDiskWrite = curDiskWrite - prevDiskWrite
) )
@ -319,7 +308,6 @@ func (db *Dashboard) collectData() {
now := time.Now() now := time.Now()
var mem runtime.MemStats
runtime.ReadMemStats(&mem) runtime.ReadMemStats(&mem)
activeMemory := &ChartEntry{ activeMemory := &ChartEntry{
Time: now, Time: now,
@ -363,7 +351,7 @@ func (db *Dashboard) collectData() {
db.charts.DiskWrite = append(db.charts.DiskRead[1:], diskWrite) db.charts.DiskWrite = append(db.charts.DiskRead[1:], diskWrite)
db.sendToAll(&Message{ db.sendToAll(&Message{
Home: &HomeMessage{ System: &SystemMessage{
ActiveMemory: ChartEntries{activeMemory}, ActiveMemory: ChartEntries{activeMemory},
VirtualMemory: ChartEntries{virtualMemory}, VirtualMemory: ChartEntries{virtualMemory},
NetworkIngress: ChartEntries{networkIngress}, NetworkIngress: ChartEntries{networkIngress},

@ -28,27 +28,20 @@ type Message struct {
Logs *LogsMessage `json:"logs,omitempty"` Logs *LogsMessage `json:"logs,omitempty"`
} }
type ChartEntries []*ChartEntry
type ChartEntry struct {
Time time.Time `json:"time,omitempty"`
Value float64 `json:"value,omitempty"`
}
type GeneralMessage struct { type GeneralMessage struct {
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"` Commit string `json:"commit,omitempty"`
} }
type HomeMessage struct { type HomeMessage struct {
ActiveMemory ChartEntries `json:"activeMemory,omitempty"` /* TODO (kurkomisi) */
VirtualMemory ChartEntries `json:"virtualMemory,omitempty"`
NetworkIngress ChartEntries `json:"networkIngress,omitempty"`
NetworkEgress ChartEntries `json:"networkEgress,omitempty"`
ProcessCPU ChartEntries `json:"processCPU,omitempty"`
SystemCPU ChartEntries `json:"systemCPU,omitempty"`
DiskRead ChartEntries `json:"diskRead,omitempty"`
DiskWrite ChartEntries `json:"diskWrite,omitempty"`
}
type ChartEntries []*ChartEntry
type ChartEntry struct {
Time time.Time `json:"time,omitempty"`
Value float64 `json:"value,omitempty"`
} }
type ChainMessage struct { type ChainMessage struct {
@ -64,7 +57,14 @@ type NetworkMessage struct {
} }
type SystemMessage struct { type SystemMessage struct {
/* TODO (kurkomisi) */ ActiveMemory ChartEntries `json:"activeMemory,omitempty"`
VirtualMemory ChartEntries `json:"virtualMemory,omitempty"`
NetworkIngress ChartEntries `json:"networkIngress,omitempty"`
NetworkEgress ChartEntries `json:"networkEgress,omitempty"`
ProcessCPU ChartEntries `json:"processCPU,omitempty"`
SystemCPU ChartEntries `json:"systemCPU,omitempty"`
DiskRead ChartEntries `json:"diskRead,omitempty"`
DiskWrite ChartEntries `json:"diskWrite,omitempty"`
} }
type LogsMessage struct { type LogsMessage struct {