Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Mar 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ profile.cov
/dashboard/assets/node_modules
/dashboard/assets/stats.json
/dashboard/assets/bundle.js
/dashboard/assets/package-lock.json

**/yarn-error.log
1 change: 0 additions & 1 deletion cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ var (
utils.DashboardAddrFlag,
utils.DashboardPortFlag,
utils.DashboardRefreshFlag,
utils.DashboardAssetsFlag,
utils.EthashCacheDirFlag,
utils.EthashCachesInMemoryFlag,
utils.EthashCachesOnDiskFlag,
Expand Down
6 changes: 0 additions & 6 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,6 @@ var (
Usage: "Dashboard metrics collection refresh rate",
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
EthashCacheDirFlag = DirectoryFlag{
Name: "ethash.cachedir",
Expand Down Expand Up @@ -1120,7 +1115,6 @@ func SetDashboardConfig(ctx *cli.Context, cfg *dashboard.Config) {
cfg.Host = ctx.GlobalString(DashboardAddrFlag.Name)
cfg.Port = ctx.GlobalInt(DashboardPortFlag.Name)
cfg.Refresh = ctx.GlobalDuration(DashboardRefreshFlag.Name)
cfg.Assets = ctx.GlobalString(DashboardAssetsFlag.Name)
}

// RegisterEthService adds an Ethereum client to the stack.
Expand Down
15 changes: 7 additions & 8 deletions dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,35 @@ 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:

```
$ (cd dashboard/assets && npm install)
$ (cd dashboard/assets && ./node_modules/.bin/flow-typed install)
$ (cd dashboard/assets && yarn install && yarn flow)
```

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 --dashboard.assets=dashboard/assets --vmodule=dashboard=5
$ geth --dashboard --vmodule=dashboard=5
$ (cd dashboard/assets && yarn dev)
```

To bundle up the final UI into Geth, run `go generate`:

```
$ go generate ./dashboard
$ (cd dashboard && go generate)
```

### 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.

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.

### Have fun

[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 _space usage_ go to [Webpack Visualizer][WV], and import `stats.json`

Expand Down
4,441 changes: 2,238 additions & 2,203 deletions dashboard/assets.go

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dashboard/assets/components/CustomTooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ export const percentPlotter = <T>(text: string, mapper: (T => T) = multiplier(1)
};

// 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.
const simplifyBytes = (x: number) => {
let i = 0;
for (; x > 1024 && i < 5; i++) {
for (; x > 1024 && i < 8; i++) {
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.
Expand Down
28 changes: 14 additions & 14 deletions dashboard/assets/components/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ const defaultContent: Content = {
version: null,
commit: null,
},
home: {
home: {},
chain: {},
txpool: {},
network: {},
system: {
activeMemory: [],
virtualMemory: [],
networkIngress: [],
Expand All @@ -91,10 +95,6 @@ const defaultContent: Content = {
diskRead: [],
diskWrite: [],
},
chain: {},
txpool: {},
network: {},
system: {},
logs: {
log: [],
},
Expand All @@ -108,7 +108,11 @@ const updaters = {
version: replacer,
commit: replacer,
},
home: {
home: null,
chain: null,
txpool: null,
network: null,
system: {
activeMemory: appender(200),
virtualMemory: appender(200),
networkIngress: appender(200),
Expand All @@ -118,11 +122,7 @@ const updaters = {
diskRead: appender(200),
diskWrite: appender(200),
},
chain: null,
txpool: null,
network: null,
system: null,
logs: {
logs: {
log: appender(200),
},
};
Expand All @@ -136,7 +136,7 @@ const styles = {
height: '100%',
zIndex: 1,
overflow: 'hidden',
}
},
};

// themeStyles returns the styles generated from the theme for the component.
Expand Down Expand Up @@ -178,7 +178,8 @@ class Dashboard extends Component<Props, State> {
// 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`);
// PROD is defined by webpack.
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${PROD ? window.location.host : 'localhost:8080'}/api`);
server.onopen = () => {
this.setState({content: defaultContent, shouldUpdate: {}});
};
Expand Down Expand Up @@ -217,7 +218,6 @@ class Dashboard extends Component<Props, State> {
return (
<div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header
opened={this.state.sideBar}
switchSideBar={this.switchSideBar}
/>
<Body
Expand Down
158 changes: 82 additions & 76 deletions dashboard/assets/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ import {ResponsiveContainer, AreaChart, Area, Tooltip} from 'recharts';
import ChartRow from './ChartRow';
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from './CustomTooltip';
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.
const styles = {
Expand All @@ -40,17 +50,16 @@ const styles = {
padding: 0,
},
doubleChartWrapper: {
height: '100%',
width: '99%',
paddingTop: 5,
height: '100%',
width: '99%',
},
};

// themeStyles returns the styles generated from the theme for the component.
const themeStyles: Object = (theme: Object) => ({
footer: {
backgroundColor: theme.palette.background.appBar,
color: theme.palette.getContrastText(theme.palette.background.appBar),
backgroundColor: theme.palette.grey[900],
color: theme.palette.getContrastText(theme.palette.grey[900]),
zIndex: theme.zIndex.appBar,
height: theme.spacing.unit * 10,
},
Expand All @@ -59,111 +68,108 @@ const themeStyles: Object = (theme: Object) => ({
export type Props = {
classes: Object, // injected by withStyles()
theme: Object,
content: Content,
general: General,
system: System,
shouldUpdate: Object,
};

// Footer renders the footer of the dashboard.
class Footer extends Component<Props> {
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.
info = (about: string, value: ?string) => (value ? (
<Typography type='caption' color='inherit'>
<span style={commonStyles.light}>{about}</span> {value}
</Typography>
) : null);
// halfHeightChart renders an area chart with half of the height of its parent.
halfHeightChart = (chartProps, tooltip, areaProps) => (
<ResponsiveContainer width='100%' height='50%'>
<AreaChart {...chartProps} >
{!tooltip || (<Tooltip cursor={false} content={<CustomTooltip tooltip={tooltip} />} />)}
<Area isAnimationActive={false} type='monotone' {...areaProps} />
</AreaChart>
</ResponsiveContainer>
);

// doubleChart renders a pair of charts separated by the baseline.
doubleChart = (syncId, topChart, bottomChart) => {
const topKey = 'topKey';
const bottomKey = 'bottomKey';
const topDefault = topChart.default ? topChart.default : 0;
const bottomDefault = bottomChart.default ? bottomChart.default : 0;
const topTooltip = topChart.tooltip ? (
<Tooltip cursor={false} content={<CustomTooltip tooltip={topChart.tooltip} />} />
) : null;
const bottomTooltip = bottomChart.tooltip ? (
<Tooltip cursor={false} content={<CustomTooltip tooltip={bottomChart.tooltip} />} />
) : null;
doubleChart = (syncId, chartKey, topChart, bottomChart) => {
if (!Array.isArray(topChart.data) || !Array.isArray(bottomChart.data)) {
return null;
}
const topDefault = topChart.default || 0;
const bottomDefault = bottomChart.default || 0;
const topKey = `${chartKey}${TOP}`;
const bottomKey = `${chartKey}${BOTTOM}`;
const topColor = '#8884d8';
const bottomColor = '#82ca9d';

// Put the samples of the two charts into the same array in order to avoid problems
// at the synchronized area charts. If one of the two arrays doesn't have value at
// a given position, give it a 0 default value.
let data = [...topChart.data.map(({value}) => {
const d = {};
d[topKey] = value || topDefault;
return d;
})];
for (let i = 0; i < data.length && i < bottomChart.data.length; i++) {
// The value needs to be negative in order to plot it upside down.
const d = bottomChart.data[i];
data[i][bottomKey] = d && d.value ? -d.value : bottomDefault;
}
data = [...data, ...bottomChart.data.slice(data.length).map(({value}) => {
const d = {};
d[topKey] = topDefault;
d[bottomKey] = -value || bottomDefault;
return d;
})];

return (
<div style={styles.doubleChartWrapper}>
<ResponsiveContainer width='100%' height='50%'>
<AreaChart data={data} syncId={syncId} >
{topTooltip}
<Area type='monotone' dataKey={topKey} stroke={topColor} fill={topColor} />
</AreaChart>
</ResponsiveContainer>
<div style={{marginTop: -10, width: '100%', height: '50%'}}>
<ResponsiveContainer width='100%' height='100%'>
<AreaChart data={data} syncId={syncId} >
{bottomTooltip}
<Area type='monotone' dataKey={bottomKey} stroke={bottomColor} fill={bottomColor} />
</AreaChart>
</ResponsiveContainer>
</div>
{this.halfHeightChart(
{
syncId,
data: topChart.data.map(({value}) => ({[topKey]: value || topDefault})),
margin: {top: 5, right: 5, bottom: 0, left: 5},
},
topChart.tooltip,
{dataKey: topKey, stroke: topColor, fill: topColor},
)}
{this.halfHeightChart(
{
syncId,
data: bottomChart.data.map(({value}) => ({[bottomKey]: -value || -bottomDefault})),
margin: {top: 0, right: 5, bottom: 5, left: 5},
},
bottomChart.tooltip,
{dataKey: bottomKey, stroke: bottomColor, fill: bottomColor},
)}
</div>
);
}
};

render() {
const {content} = this.props;
const {general, home} = content;
const {general, system} = this.props;

return (
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
<Grid item xs style={styles.chartRowWrapper}>
<ChartRow>
{this.doubleChart(
'all',
{data: home.processCPU, tooltip: percentPlotter('Process')},
{data: home.systemCPU, tooltip: percentPlotter('System', multiplier(-1))},
FOOTER_SYNC_ID,
CPU,
{data: system.processCPU, tooltip: percentPlotter('Process load')},
{data: system.systemCPU, tooltip: percentPlotter('System load', multiplier(-1))},
)}
{this.doubleChart(
'all',
{data: home.activeMemory, tooltip: bytePlotter('Active')},
{data: home.virtualMemory, tooltip: bytePlotter('Virtual', multiplier(-1))},
FOOTER_SYNC_ID,
MEMORY,
{data: system.activeMemory, tooltip: bytePlotter('Active memory')},
{data: system.virtualMemory, tooltip: bytePlotter('Virtual memory', multiplier(-1))},
)}
{this.doubleChart(
'all',
{data: home.diskRead, tooltip: bytePerSecPlotter('Disk Read')},
{data: home.diskWrite, tooltip: bytePerSecPlotter('Disk Write', multiplier(-1))},
FOOTER_SYNC_ID,
DISK,
{data: system.diskRead, tooltip: bytePerSecPlotter('Disk read')},
{data: system.diskWrite, tooltip: bytePerSecPlotter('Disk write', multiplier(-1))},
)}
{this.doubleChart(
'all',
{data: home.networkIngress, tooltip: bytePerSecPlotter('Download')},
{data: home.networkEgress, tooltip: bytePerSecPlotter('Upload', multiplier(-1))},
FOOTER_SYNC_ID,
TRAFFIC,
{data: system.networkIngress, tooltip: bytePerSecPlotter('Download')},
{data: system.networkEgress, tooltip: bytePerSecPlotter('Upload', multiplier(-1))},
)}
</ChartRow>
</Grid>
<Grid item >
{this.info('Geth', general.version)}
{this.info('Commit', general.commit ? general.commit.substring(0, 7) : null)}
<Typography type='caption' color='inherit'>
<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>
);
Expand Down
Loading