Skip to content

Commit

Permalink
dashboard: integrate Flow, sketch message API (ethereum#15713)
Browse files Browse the repository at this point in the history
* dashboard: minor design change

* dashboard: Flow integration, message API

* dashboard: minor polishes, exclude misspell linter
  • Loading branch information
kurkomisi authored and karalabe committed Dec 21, 2017
1 parent 52f4d6d commit 9dbb8ef
Show file tree
Hide file tree
Showing 23 changed files with 49,960 additions and 620 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ profile.cov
.idea

# dashboard
/dashboard/assets/flow-typed
/dashboard/assets/node_modules
/dashboard/assets/stats.json
/dashboard/assets/public/bundle.js
19 changes: 16 additions & 3 deletions dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ The client's UI uses [React][React] with JSX syntax, which is validated by the [

### Development and bundling

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 && ./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:
Expand All @@ -22,13 +23,20 @@ $ (cd dashboard/assets && ./node_modules/.bin/webpack --watch)
$ 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
```

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

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.
Expand All @@ -44,3 +52,8 @@ $ go generate ./dashboard
[WA]: http://webpack.github.io/analyse/
[WV]: http://chrisbateman.github.io/webpack-visualizer/
[Node.js]: https://nodejs.org/en/
[Flow]: https://flow.org/
[Atom]: https://atom.io/
[Atom config]: https://medium.com/@fastphrase/integrating-flow-into-a-react-project-fbbc2f130eed
[Nuclide]: https://nuclide.io/docs/quick-start/getting-started/
[flow-typed]: https://github.com/flowtype/flow-typed
42,113 changes: 42,080 additions & 33 deletions dashboard/assets.go

Large diffs are not rendered by default.

97 changes: 64 additions & 33 deletions dashboard/assets/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,68 @@

// React syntax style mostly according to https://github.com/airbnb/javascript/tree/master/react
{
"plugins": [
"react"
],
"parser": "babel-eslint",
"parserOptions": {
"ecmaFeatures": {
"jsx": true,
"modules": true
}
},
"rules": {
"react/prefer-es6-class": 2,
"react/prefer-stateless-function": 2,
"react/jsx-pascal-case": 2,
"react/jsx-closing-bracket-location": [1, {"selfClosing": "tag-aligned", "nonEmpty": "tag-aligned"}],
"react/jsx-closing-tag-location": 1,
"jsx-quotes": ["error", "prefer-double"],
"no-multi-spaces": "error",
"react/jsx-tag-spacing": 2,
"react/jsx-curly-spacing": [2, {"when": "never", "children": true}],
"react/jsx-boolean-value": 2,
"react/no-string-refs": 2,
"react/jsx-wrap-multilines": 2,
"react/self-closing-comp": 2,
"react/jsx-no-bind": 2,
"react/require-render-return": 2,
"react/no-is-mounted": 2,
"key-spacing": ["error", {"align": {
"beforeColon": false,
"afterColon": true,
"on": "value"
}}]
}
'env': {
'browser': true,
'node': true,
'es6': true,
},
'parser': 'babel-eslint',
'parserOptions': {
'sourceType': 'module',
'ecmaVersion': 6,
'ecmaFeatures': {
'jsx': true,
}
},
'extends': 'airbnb',
'plugins': [
'flowtype',
'react',
],
'rules': {
'no-tabs': 'off',
'indent': ['error', 'tab'],
'react/jsx-indent': ['error', 'tab'],
'react/jsx-indent-props': ['error', 'tab'],
'react/prefer-stateless-function': 'off',

// Specifies the maximum length of a line.
'max-len': ['warn', 120, 2, {
'ignoreUrls': true,
'ignoreComments': false,
'ignoreRegExpLiterals': true,
'ignoreStrings': true,
'ignoreTemplateLiterals': true,
}],
// Enforces spacing between keys and values in object literal properties.
'key-spacing': ['error', {'align': {
'beforeColon': false,
'afterColon': true,
'on': 'value'
}}],
// Prohibits padding inside curly braces.
'object-curly-spacing': ['error', 'never'],
'no-use-before-define': 'off', // messageAPI
'default-case': 'off',

'flowtype/boolean-style': ['error', 'boolean'],
'flowtype/define-flow-type': 'warn',
'flowtype/generic-spacing': ['error', 'never'],
'flowtype/no-primitive-constructor-types': 'error',
'flowtype/no-weak-types': 'error',
'flowtype/object-type-delimiter': ['error', 'comma'],
'flowtype/require-valid-file-annotation': 'error',
'flowtype/semi': ['error', 'always'],
'flowtype/space-after-type-colon': ['error', 'always'],
'flowtype/space-before-generic-bracket': ['error', 'never'],
'flowtype/space-before-type-colon': ['error', 'never'],
'flowtype/union-intersection-spacing': ['error', 'always'],
'flowtype/use-flow-type': 'warn',
'flowtype/valid-syntax': 'warn',
},
'settings': {
'flowtype': {
'onlyFilesWithFlowAnnotation': true,
}
},
}
9 changes: 9 additions & 0 deletions dashboard/assets/.flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[ignore]
<PROJECT_ROOT>/node_modules/material-ui/.*\.js\.flow

[libs]
<PROJECT_ROOT>/flow-typed/
node_modules/jss/flow-typed

[options]
include_warnings=true
64 changes: 64 additions & 0 deletions dashboard/assets/components/Body.jsx
Original file line number Diff line number Diff line change
@@ -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);
49 changes: 49 additions & 0 deletions dashboard/assets/components/ChartGrid.jsx
Original file line number Diff line number Diff line change
@@ -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;
107 changes: 74 additions & 33 deletions dashboard/assets/components/Common.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow

// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
Expand All @@ -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]));
Loading

0 comments on commit 9dbb8ef

Please sign in to comment.