dashboard: Flow integration, message API

This commit is contained in:
Kurkó Mihály 2017-12-20 09:52:58 +02:00 committed by Péter Szilágyi
parent 23bce8353a
commit 5a6362e2d5
No known key found for this signature in database
GPG key ID: E9AE538CEDF8293D
25 changed files with 19911 additions and 7451 deletions

View file

@ -28,6 +28,19 @@ To bundle up the final UI into Geth, run `go generate`:
$ go generate ./dashboard
```
Since JavaScript doesn't provide type safety, [Flow][Flow] is used to introduce and check types. These types are only useful during the development, so at the end of the day Babel will strip them.
To take advantage of types the IDE needs to be prepared for them.
In case of [Atom][Atom] a configuration guide can be found [here][Atom config].
Install the [Nuclide][Nuclide] package for Flow support, make 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 their auto-complete [flow-typed][flow-typed] is used.
To visualize the hidden elements (e.g. `node_modules`), uncheck the `Exclude VCS Ignored Path` in `Settings > Core`.
To visualize the white spaces, check `Show invisibles` in `Settings > Editor`.
To use Sublime-like minimap, install the `minimap` package.
In case of trouble related to the Atom UI, run `atom --clear-window-state`.
### Have fun
[Webpack][Webpack] offers handy tools for visualizing the bundle's dependency tree and space usage.
@ -43,3 +56,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

File diff suppressed because one or more lines are too long

View file

@ -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,
}
},
}

View file

@ -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

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -15,54 +17,48 @@
// 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";
import SideBar from './SideBar';
import Main from './Main';
import type {Content} from '../types/content';
// Styles for the Body component.
const styles = theme => ({
body: {
display: 'flex',
width: '100%',
height: '100%',
},
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.
@withStyles(styles)
class Body extends Component {
render() {
const {classes} = this.props; // The classes property is injected by withStyles().
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}
/>
<Content
active={this.props.active}
memory={this.props.memory}
traffic={this.props.traffic}
logs={this.props.logs}
shouldUpdate={this.props.shouldUpdate}
/>
</div>
);
}
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>
);
}
}
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;
export default withStyles(styles)(Body);

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -15,34 +17,33 @@
// 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 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 {
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>
);
}
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>
);
}
}
ChartGrid.propTypes = {
spacing: PropTypes.number.isRequired,
};
export default ChartGrid;

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -14,39 +16,78 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// isNullOrUndefined returns true if the given variable is null or undefined.
export const isNullOrUndefined = variable => variable === null || typeof variable === 'undefined';
export const LIMIT = {
memory: 200, // Maximum number of memory data samples.
traffic: 200, // Maximum number of traffic data samples.
log: 200, // Maximum number of logs.
};
type ProvidedMenuProp = {|title: string, icon: string|};
const menuSkeletons: Array<{|id: string, menu: ProvidedMenuProp|}> = [
{
id: 'home',
menu: {
title: 'Home',
icon: 'home',
},
}, {
id: 'chain',
menu: {
title: 'Chain',
icon: 'link',
},
}, {
id: 'txpool',
menu: {
title: 'TxPool',
icon: 'credit-card',
},
}, {
id: 'network',
menu: {
title: 'Network',
icon: 'globe',
},
}, {
id: 'system',
menu: {
title: 'System',
icon: 'tachometer',
},
}, {
id: 'logs',
menu: {
title: 'Logs',
icon: 'list',
},
},
];
export type MenuProp = {|...ProvidedMenuProp, id: string|};
// The sidebar menu and the main content are rendered based on these elements.
export const TAGS = (() => {
const T = {
home: { title: "Home", icon: "home", },
chain: { title: "Chain", icon: "link", },
transactions: { title: "Transactions", icon: "credit-card", },
network: { title: "Network", icon: "globe", },
system: { title: "System", icon: "tachometer", },
logs: { title: "Logs", icon: "list", },
};
// 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}])));
export const DATA_KEYS = (() => {
const DK = {};
["memory", "traffic", "logs"].map(key => {
DK[key] = key;
});
return DK;
})();
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 DURATION = 200;
export const LENS: Map<string, string> = new Map([
'content',
...menuSkeletons.map(({id}) => id),
...sampleSkeletons.map(({id}) => id),
].map(lens => [lens, lens]));

View file

@ -1,71 +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 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

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -17,146 +19,181 @@
import React, {Component} from 'react';
import withStyles from 'material-ui/styles/withStyles';
import {lensPath, view, set} from 'ramda';
import Header from './Header.jsx';
import Body from './Body.jsx';
import {isNullOrUndefined, LIMIT, TAGS, DATA_KEYS} from "./Common.jsx";
import Header from './Header';
import Body from './Body';
import {MENU, SAMPLE} from './Common';
import type {Message, HomeMessage, LogsMessage, Chart} from '../types/message';
import type {Content} from '../types/content';
// Styles for the Dashboard component.
// appender appends an array (A) to the end of another array (B) in the state.
// lens is the path of B in the state, samples is A, and limit is the maximum size of the changed array.
//
// appender retrieves a function, which overrides the state's value at lens, and returns with the overridden state.
const appender = (lens, samples, limit) => (state) => {
const newSamples = [
...view(lens, state), // retrieves a specific value of the state at the given path (lens).
...samples,
];
// set is a function of ramda.js, which needs the path, the new value, the original state, and retrieves
// the altered state.
return set(
lens,
newSamples.slice(newSamples.length > limit ? newSamples.length - limit : 0),
state
);
};
// Lenses for specific data fields in the state, used for a clearer deep update.
// NOTE: This solution will be changed very likely.
const memoryLens = lensPath(['content', 'home', 'memory']);
const trafficLens = lensPath(['content', 'home', 'traffic']);
const logLens = lensPath(['content', 'logs', 'log']);
// styles retrieves the styles for the Dashboard component.
const styles = theme => ({
dashboard: {
display: 'flex',
flexFlow: 'column',
width: '100%',
height: '100%',
background: theme.palette.background.default,
zIndex: 1,
overflow: 'hidden',
},
dashboard: {
display: 'flex',
flexFlow: 'column',
width: '100%',
height: '100%',
background: theme.palette.background.default,
zIndex: 1,
overflow: 'hidden',
},
});
export type Props = {
classes: Object,
};
type State = {
active: string, // active menu
sideBar: boolean, // true if the sidebar is opened
content: $Shape<Content>, // the visualized data
shouldUpdate: Set<string> // labels for the components, which need to rerender based on the incoming message
};
// Dashboard is the main component, which renders the whole page, makes connection with the server and
// listens for messages. When there is an incoming message, updates the page's content correspondingly.
class Dashboard extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
active: MENU.get('home').id,
sideBar: true,
content: {home: {memory: [], traffic: []}, logs: {log: []}},
shouldUpdate: new Set(),
};
}
// 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.
@withStyles(styles)
class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {
active: TAGS.home.id, // active menu
sideBar: true, // true if the sidebar is opened
memory: [],
traffic: [],
logs: [],
shouldUpdate: {}, // contains the labels of the incoming sample types
};
}
// componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
componentDidMount() {
this.reconnect();
}
// componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
componentDidMount() {
this.reconnect();
}
// reconnect establishes a websocket connection with the server, listens for incoming messages
// and tries to reconnect on connection loss.
reconnect = () => {
this.setState({
content: {home: {memory: [], traffic: []}, logs: {log: []}},
});
const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`);
server.onmessage = (event) => {
const msg: Message = JSON.parse(event.data);
if (!msg) {
return;
}
this.update(msg);
};
server.onclose = () => {
setTimeout(this.reconnect, 3000);
};
};
// 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");
// samples retrieves the raw data of a chart field from the incoming message.
samples = (chart: Chart) => {
let s = [];
if (chart.history) {
s = chart.history.map(({value}) => (value || 0)); // traffic comes without value at the beginning
}
if (chart.new) {
s = [...s, chart.new.value || 0];
}
return s;
};
server.onmessage = event => {
const msg = JSON.parse(event.data);
if (isNullOrUndefined(msg)) {
return;
}
this.update(msg);
};
// handleHome changes the home-menu related part of the state.
handleHome = (home: HomeMessage) => {
this.setState((prevState) => {
let newState = prevState;
newState.shouldUpdate = new Set();
if (home.memory) {
newState = appender(memoryLens, this.samples(home.memory), SAMPLE.get('memory').limit)(newState);
newState.shouldUpdate.add('memory');
}
if (home.traffic) {
newState = appender(trafficLens, this.samples(home.traffic), SAMPLE.get('traffic').limit)(newState);
newState.shouldUpdate.add('traffic');
}
return newState;
});
};
server.onclose = () => {
setTimeout(this.reconnect, 3000);
};
};
// handleLogs changes the logs-menu related part of the state.
handleLogs = (logs: LogsMessage) => {
this.setState((prevState) => {
let newState = prevState;
newState.shouldUpdate = new Set();
if (logs.log) {
newState = appender(logLens, [logs.log], SAMPLE.get('logs').limit)(newState);
newState.shouldUpdate.add('logs');
}
return newState;
});
};
// update analyzes the incoming message, and updates the charts' content correspondingly.
update = msg => {
this.setState(prevState => {
let newState = [];
newState.shouldUpdate = {};
const insert = (key, values, limit) => {
newState[key] = [...prevState[key], ...values];
while (newState[key].length > limit) {
newState[key].shift();
}
newState.shouldUpdate[key] = true;
};
// (Re)initialize the state with the past data.
if (!isNullOrUndefined(msg.history)) {
const memory = DATA_KEYS.memory;
const traffic = DATA_KEYS.traffic;
newState[memory] = [];
newState[traffic] = [];
if (!isNullOrUndefined(msg.history.memorySamples)) {
newState[memory] = msg.history.memorySamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
while (newState[memory].length > LIMIT.memory) {
newState[memory].shift();
}
newState.shouldUpdate[memory] = true;
}
if (!isNullOrUndefined(msg.history.trafficSamples)) {
newState[traffic] = msg.history.trafficSamples.map(elem => isNullOrUndefined(elem.value) ? 0 : elem.value);
while (newState[traffic].length > LIMIT.traffic) {
newState[traffic].shift();
}
newState.shouldUpdate[traffic] = true;
}
}
// Insert the new data samples.
if (!isNullOrUndefined(msg.memory)) {
insert(DATA_KEYS.memory, [isNullOrUndefined(msg.memory.value) ? 0 : msg.memory.value], LIMIT.memory);
}
if (!isNullOrUndefined(msg.traffic)) {
insert(DATA_KEYS.traffic, [isNullOrUndefined(msg.traffic.value) ? 0 : msg.traffic.value], LIMIT.traffic);
}
if (!isNullOrUndefined(msg.log)) {
insert(DATA_KEYS.logs, [msg.log], LIMIT.log);
}
// update analyzes the incoming message, and updates the charts' content correspondingly.
update = (msg: Message) => {
if (msg.home) {
this.handleHome(msg.home);
}
if (msg.logs) {
this.handleLogs(msg.logs);
}
};
return newState;
});
};
// changeContent sets the active label, which is used at the content rendering.
changeContent = (newActive: string) => {
this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
};
// changeContent sets the active label, which is used at the content rendering.
changeContent = newActive => {
this.setState(prevState => prevState.active !== newActive ? {active: newActive} : {});
};
// openSideBar opens the sidebar.
openSideBar = () => {
this.setState({sideBar: true});
};
openSideBar = () => {
this.setState({sideBar: true});
};
// closeSideBar closes the sidebar.
closeSideBar = () => {
this.setState({sideBar: false});
};
closeSideBar = () => {
this.setState({sideBar: false});
};
render() {
const {classes} = this.props; // The classes property is injected by withStyles().
render() {
const {classes} = this.props; // The classes property is injected by withStyles().
return (
<div className={classes.dashboard}>
<Header
opened={this.state.sideBar}
openSideBar={this.openSideBar}
closeSideBar={this.closeSideBar}
/>
<Body
opened={this.state.sideBar}
changeContent={this.changeContent}
active={this.state.active}
memory={this.state.memory}
traffic={this.state.traffic}
logs={this.state.logs}
shouldUpdate={this.state.shouldUpdate}
/>
</div>
);
}
return (
<div className={classes.dashboard}>
<Header
opened={this.state.sideBar}
openSideBar={this.openSideBar}
closeSideBar={this.closeSideBar}
/>
<Body
opened={this.state.sideBar}
changeContent={this.changeContent}
active={this.state.active}
content={this.state.content}
shouldUpdate={this.state.shouldUpdate}
/>
</div>
);
}
}
export default Dashboard;
export default withStyles(styles)(Dashboard);

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -15,88 +17,89 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import withStyles from 'material-ui/styles/withStyles';
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 Typography from 'material-ui/Typography';
import ChevronLeftIcon from 'material-ui-icons/ChevronLeft';
import {DURATION} from './Common.jsx';
import {DURATION} from './Common';
// arrowDefault is the default style of the arrow button.
const arrowDefault = {
transition: `transform ${DURATION}ms`,
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)" },
entered: {transform: 'rotate(180deg)'},
};
// Styles for the Header component.
const styles = theme => ({
header: {
backgroundColor: theme.palette.background.appBar,
color: theme.palette.getContrastText(theme.palette.background.appBar),
zIndex: theme.zIndex.appBar,
},
toolbar: {
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
},
mainText: {
paddingLeft: theme.spacing.unit,
},
header: {
backgroundColor: theme.palette.background.appBar,
color: theme.palette.getContrastText(theme.palette.background.appBar),
zIndex: theme.zIndex.appBar,
},
toolbar: {
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
},
mainText: {
paddingLeft: theme.spacing.unit,
},
});
export type Props = {
classes: Object,
opened: boolean,
openSideBar: () => {},
closeSideBar: () => {},
};
// Header renders the header of the dashboard.
@withStyles(styles)
class Header extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.opened !== this.props.opened;
}
class Header extends Component<Props> {
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();
};
// changeSideBar opens or closes the sidebar corresponding to the previous state.
changeSideBar = () => {
if (this.props.opened) {
this.props.closeSideBar();
} else {
this.props.openSideBar();
}
};
// arrowButton is connected to the sidebar; changes its state.
arrowButton = transitionState => (
<IconButton onClick={this.changeSideBar}>
<ChevronLeftIcon
style={{
...arrowDefault,
...arrowTransition[transitionState],
}}
/>
</IconButton>
);
// arrowButton is connected to the sidebar; changes its state.
arrowButton = (transitionState: string) => (
<IconButton onClick={this.changeSideBar}>
<ChevronLeftIcon
style={{
...arrowDefault,
...arrowTransition[transitionState],
}}
/>
</IconButton>
);
render() {
const {classes, opened} = this.props; // The classes property is injected by withStyles().
render() {
const {classes, opened} = this.props; // The classes property is injected by withStyles().
return (
<AppBar position="static" className={classes.header}>
<Toolbar className={classes.toolbar}>
<Transition mountOnEnter in={opened} timeout={{enter: DURATION}}>
{this.arrowButton}
</Transition>
<Typography type="title" color="inherit" noWrap className={classes.mainText}>
PoC Go Ethereum Dashboard
</Typography>
</Toolbar>
</AppBar>
);
}
return (
<AppBar position="static" className={classes.header}>
<Toolbar className={classes.toolbar}>
<Transition mountOnEnter in={opened} timeout={{enter: DURATION}}>
{this.arrowButton}
</Transition>
<Typography type="title" color="inherit" noWrap className={classes.mainText}>
Go Ethereum Dashboard
</Typography>
</Toolbar>
</AppBar>
);
}
}
Header.propTypes = {
opened: PropTypes.bool.isRequired,
openSideBar: PropTypes.func.isRequired,
closeSideBar: PropTypes.func.isRequired,
};
export default Header;
export default withStyles(styles)(Header);

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -15,57 +17,56 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import 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 ChartGrid from './ChartGrid';
import type {ChartEntry} from '../types/message';
export type Props = {
theme: Object,
memory: Array<ChartEntry>,
traffic: Array<ChartEntry>,
shouldUpdate: Object,
};
// Home renders the home content.
@withTheme()
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];
}
class Home extends Component<Props> {
constructor(props: Props) {
super(props);
const {theme} = props; // The theme property is injected by withTheme().
this.memoryColor = theme.palette.primary[300];
this.trafficColor = theme.palette.secondary[300];
}
shouldComponentUpdate(nextProps) {
return !isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.memory]) ||
!isNullOrUndefined(nextProps.shouldUpdate[DATA_KEYS.traffic]);
}
shouldComponentUpdate(nextProps) {
return nextProps.shouldUpdate.has('memory') || nextProps.shouldUpdate.has('traffic');
}
render() {
const {memory, traffic} = this.props;
render() {
const {memory, traffic} = this.props;
return (
<ChartGrid spacing={24}>
<AreaChart xs={6} height={300} values={memory}>
<YAxis />
<Area type="monotone" dataKey="value" stroke={this.memoryColor} fill={this.memoryColor} />
</AreaChart>
<LineChart xs={6} height={300} values={traffic}>
<Line type="monotone" dataKey="value" stroke={this.trafficColor} dot={false} />
</LineChart>
<LineChart xs={6} height={300} values={memory}>
<YAxis />
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
<Line type="monotone" dataKey="value" stroke={this.memoryColor} dot={false} />
</LineChart>
<AreaChart xs={6} height={300} values={traffic}>
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
<Area type="monotone" dataKey="value" stroke={this.trafficColor} fill={this.trafficColor} />
</AreaChart>
</ChartGrid>
);
}
return (
<ChartGrid spacing={24}>
<AreaChart xs={6} height={300} values={memory}>
<YAxis />
<Area type="monotone" dataKey="value" stroke={this.memoryColor} fill={this.memoryColor} />
</AreaChart>
<LineChart xs={6} height={300} values={traffic}>
<Line type="monotone" dataKey="value" stroke={this.trafficColor} dot={false} />
</LineChart>
<LineChart xs={6} height={300} values={memory}>
<YAxis />
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
<Line type="monotone" dataKey="value" stroke={this.memoryColor} dot={false} />
</LineChart>
<AreaChart xs={6} height={300} values={traffic}>
<CartesianGrid stroke="#eee" strokeDasharray="5 5" vertical={false} />
<Area type="monotone" dataKey="value" stroke={this.trafficColor} fill={this.trafficColor} />
</AreaChart>
</ChartGrid>
);
}
}
Home.propTypes = {
shouldUpdate: PropTypes.object.isRequired,
};
export default Home;
export default withTheme()(Home);

View file

@ -0,0 +1,68 @@
// @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 Home from './Home';
import {MENU} from './Common';
import type {Content} from '../types/content';
// Styles for the Content component.
const styles = theme => ({
content: {
flexGrow: 1,
backgroundColor: theme.palette.background.default,
padding: theme.spacing.unit * 3,
overflow: 'auto',
},
});
export type Props = {
classes: Object,
active: string,
content: Content,
shouldUpdate: Object,
};
// Main renders the chosen content.
class Main extends Component<Props> {
render() {
const {
classes, active, content, shouldUpdate,
} = this.props;
let children = null;
switch (active) {
case MENU.get('home').id:
children = <Home memory={content.home.memory} traffic={content.home.traffic} shouldUpdate={shouldUpdate} />;
break;
case MENU.get('chain').id:
case MENU.get('txpool').id:
case MENU.get('network').id:
case MENU.get('system').id:
children = <div>Work in progress.</div>;
break;
case MENU.get('logs').id:
children = <div>{content.logs.log.map((log, index) => <div key={index}>{log}</div>)}</div>;
}
return <div className={classes.content}>{children}</div>;
}
}
export default withStyles(styles)(Main);

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -15,103 +17,106 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import withStyles from 'material-ui/styles/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 {Icon as FontAwesome} from 'react-fa';
import {TAGS, DURATION} from './Common.jsx';
import {MENU, DURATION} from './Common';
// menuDefault is the default style of the menu.
const menuDefault = {
transition: `margin-left ${DURATION}ms`,
transition: `margin-left ${DURATION}ms`,
};
// menu Transition is the additional style of the menu corresponding to the transition's state.
// menuTransition is the additional style of the menu corresponding to the transition's state.
const menuTransition = {
entered: {marginLeft: -200},
entered: {marginLeft: -200},
};
// Styles for the SideBar component.
const styles = theme => ({
list: {
background: theme.palette.background.appBar,
},
listItem: {
minWidth: theme.spacing.unit * 3,
},
icon: {
fontSize: theme.spacing.unit * 3,
},
list: {
background: theme.palette.background.appBar,
},
listItem: {
minWidth: theme.spacing.unit * 3,
},
icon: {
fontSize: theme.spacing.unit * 3,
},
});
export type Props = {
classes: Object,
opened: boolean,
changeContent: () => {},
};
// SideBar renders the sidebar of the dashboard.
@withStyles(styles)
class SideBar extends Component {
constructor(props) {
super(props);
class SideBar extends Component<Props> {
constructor(props) {
super(props);
// clickOn contains onClick event functions for the menu items.
// Instantiate only once, and reuse the existing functions to prevent the creation of
// new function instances every time the render method is triggered.
this.clickOn = {};
for(let key in TAGS) {
const id = TAGS[key].id;
this.clickOn[id] = event => {
event.preventDefault();
props.changeContent(id);
};
}
}
// clickOn contains onClick event functions for the menu items.
// Instantiate only once, and reuse the existing functions to prevent the creation of
// new function instances every time the render method is triggered.
this.clickOn = {};
MENU.forEach((menu) => {
this.clickOn[menu.id] = (event) => {
event.preventDefault();
props.changeContent(menu.id);
};
});
}
shouldComponentUpdate(nextProps) {
return nextProps.opened !== this.props.opened;
}
shouldComponentUpdate(nextProps) {
return nextProps.opened !== this.props.opened;
}
// menu renders the list of the menu items.
menu = transitionState => {
const {classes} = this.props; // The classes property is injected by withStyles().
menuItems = (transitionState) => {
const {classes} = this.props;
const children = [];
MENU.forEach((menu) => {
children.push(
<ListItem button key={menu.id} onClick={this.clickOn[menu.id]} className={classes.listItem}>
<ListItemIcon>
<Icon className={classes.icon}>
<FontAwesome name={menu.icon} />
</Icon>
</ListItemIcon>
<ListItemText
primary={menu.title}
style={{
...menuDefault,
...menuTransition[transitionState],
padding: 0,
}}
/>
</ListItem>,
);
});
return children;
};
return (
<div className={classes.list}>
<List>
{
Object.values(TAGS).map(tag => (
<ListItem button key={tag.id} onClick={this.clickOn[tag.id]} className={classes.listItem}>
<ListItemIcon>
<Icon className={classes.icon}>
<FontAwesome name={tag.icon} />
</Icon>
</ListItemIcon>
<ListItemText
primary={tag.title}
style={{
...menuDefault,
...menuTransition[transitionState],
padding: 0,
}}
/>
</ListItem>
))
}
</List>
</div>
);
};
// menu renders the list of the menu items.
menu = (transitionState) => {
const {classes} = this.props; // The classes property is injected by withStyles().
render() {
return (
<Transition mountOnEnter in={this.props.opened} timeout={{enter: DURATION}}>
{this.menu}
</Transition>
);
}
return (
<div className={classes.list}>
<List>
{this.menuItems(transitionState)}
</List>
</div>
);
};
render() {
return (
<Transition mountOnEnter in={this.props.opened} timeout={{enter: DURATION}}>
{this.menu}
</Transition>
);
}
}
SideBar.propTypes = {
opened: PropTypes.bool.isRequired,
changeContent: PropTypes.func.isRequired,
};
export default SideBar;
export default withStyles(styles)(SideBar);

View file

@ -14,12 +14,12 @@
// 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.
// fa-only-woff-loader removes the .eot, .ttf, .svg dependencies of the FontAwesome library,
// 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;
return content
.replace(/src.*url(?!.*url.*(\.eot)).*(\.eot)[^;]*;/,'')
.replace(/url(?!.*url.*(\.eot)).*(\.eot)[^,]*,/,'')
.replace(/url(?!.*url.*(\.ttf)).*(\.ttf)[^,]*,/,'')
.replace(/,[^,]*url(?!.*url.*(\.svg)).*(\.svg)[^;]*;/,';');
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
// flow-typed signature: e931b17fb0a809fe442a2efc46879228
// flow-typed version: <<STUB>>/path_v^0.12.7/flow_v0.59.0
/**
* This is an autogenerated libdef stub for:
*
* 'path'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'path' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'path/path' {
declare module.exports: any;
}
// Filename aliases
declare module 'path/path.js' {
declare module.exports: $Exports<'path/path'>;
}

View file

@ -0,0 +1,87 @@
// flow-typed signature: 342a3d0c93da454166459879159cf1af
// flow-typed version: <<STUB>>/react-transition-group_v^2.2.1/flow_v0.59.0
/**
* This is an autogenerated libdef stub for:
*
* 'react-transition-group'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-transition-group' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'react-transition-group/CSSTransition' {
declare module.exports: any;
}
declare module 'react-transition-group/dist/react-transition-group' {
declare module.exports: any;
}
declare module 'react-transition-group/dist/react-transition-group.min' {
declare module.exports: any;
}
declare module 'react-transition-group/Transition' {
declare module.exports: any;
}
declare module 'react-transition-group/TransitionGroup' {
declare module.exports: any;
}
declare module 'react-transition-group/utils/ChildMapping' {
declare module.exports: any;
}
declare module 'react-transition-group/utils/PropTypes' {
declare module.exports: any;
}
declare module 'react-transition-group/utils/SimpleSet' {
declare module.exports: any;
}
// Filename aliases
declare module 'react-transition-group/CSSTransition.js' {
declare module.exports: $Exports<'react-transition-group/CSSTransition'>;
}
declare module 'react-transition-group/dist/react-transition-group.js' {
declare module.exports: $Exports<'react-transition-group/dist/react-transition-group'>;
}
declare module 'react-transition-group/dist/react-transition-group.min.js' {
declare module.exports: $Exports<'react-transition-group/dist/react-transition-group.min'>;
}
declare module 'react-transition-group/index' {
declare module.exports: $Exports<'react-transition-group'>;
}
declare module 'react-transition-group/index.js' {
declare module.exports: $Exports<'react-transition-group'>;
}
declare module 'react-transition-group/Transition.js' {
declare module.exports: $Exports<'react-transition-group/Transition'>;
}
declare module 'react-transition-group/TransitionGroup.js' {
declare module.exports: $Exports<'react-transition-group/TransitionGroup'>;
}
declare module 'react-transition-group/utils/ChildMapping.js' {
declare module.exports: $Exports<'react-transition-group/utils/ChildMapping'>;
}
declare module 'react-transition-group/utils/PropTypes.js' {
declare module.exports: $Exports<'react-transition-group/utils/PropTypes'>;
}
declare module 'react-transition-group/utils/SimpleSet.js' {
declare module.exports: $Exports<'react-transition-group/utils/SimpleSet'>;
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,5 @@
// @flow
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
@ -17,21 +19,23 @@
import React from 'react';
import {render} from 'react-dom';
import {createMuiTheme, MuiThemeProvider} from 'material-ui/styles';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import createMuiTheme from 'material-ui/styles/createMuiTheme';
import Dashboard from './components/Dashboard.jsx';
import Dashboard from './components/Dashboard';
// Theme for the dashboard.
const theme = createMuiTheme({
palette: {
type: 'dark',
},
});
// Renders the whole dashboard.
render(
<MuiThemeProvider theme={theme}>
<Dashboard />
</MuiThemeProvider>,
document.getElementById('dashboard')
);
const dashboard = document.getElementById('dashboard');
if (dashboard) {
// Renders the whole dashboard.
render(
<MuiThemeProvider theme={theme}>
<Dashboard />
</MuiThemeProvider>,
dashboard,
);
}

View file

@ -1,30 +1,41 @@
{
"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-eslint": "^8.0.2",
"babel-loader": "^7.1.2",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"css-loader": "^0.28.7",
"path": "^0.12.7",
"style-loader": "^0.19.0",
"url": "^0.11.0",
"url-loader": "^0.6.2",
"webpack": "^3.5.5"
}
"dependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^7.1.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"classnames": "^2.2.5",
"css-loader": "^0.28.7",
"eslint": "^4.13.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"eslint-plugin-flowtype": "^2.40.1",
"file-loader": "^1.1.6",
"flow-bin": "^0.61.0",
"flow-bin-loader": "^1.0.2",
"flow-typed": "^2.2.3",
"material-ui": "^1.0.0-beta.24",
"material-ui-icons": "^1.0.0-beta.17",
"path": "^0.12.7",
"ramda": "^0.25.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-fa": "^5.0.0",
"react-transition-group": "^2.2.1",
"recharts": "^1.0.0-beta.6",
"style-loader": "^0.19.1",
"url": "^0.11.0",
"url-loader": "^0.6.2",
"webpack": "^3.10.0"
}
}

View file

@ -0,0 +1,53 @@
// @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 type {ChartEntry} from './message';
export type Content = {
home: Home,
chain: Chain,
txpool: TxPool,
network: Network,
system: System,
logs: Logs,
};
export type Home = {
memory: Array<ChartEntry>,
traffic: Array<ChartEntry>,
};
export type Chain = {
/* TODO (kurkomisi) */
};
export type TxPool = {
/* TODO (kurkomisi) */
};
export type Network = {
/* TODO (kurkomisi) */
};
export type System = {
/* TODO (kurkomisi) */
};
export type Logs = {
log: Array<string>,
};

View file

@ -0,0 +1,61 @@
// @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/>.
export type Message = {
home?: HomeMessage,
chain?: ChainMessage,
txpool?: TxPoolMessage,
network?: NetworkMessage,
system?: SystemMessage,
logs?: LogsMessage,
};
export type HomeMessage = {
memory?: Chart,
traffic?: Chart,
};
export type Chart = {
history?: Array<ChartEntry>,
new?: ChartEntry,
};
export type ChartEntry = {
time: Date,
value: number,
};
export type ChainMessage = {
/* TODO (kurkomisi) */
};
export type TxPoolMessage = {
/* TODO (kurkomisi) */
};
export type NetworkMessage = {
/* TODO (kurkomisi) */
};
export type SystemMessage = {
/* TODO (kurkomisi) */
};
export type LogsMessage = {
log: string,
};

View file

@ -18,40 +18,57 @@ const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: './index.jsx',
output: {
path: path.resolve(__dirname, 'public'),
filename: 'bundle.js',
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
comments: false,
mangle: false,
beautify: true,
}),
],
module: {
loaders: [
{
test: /\.jsx$/, // regexp for JSX files
loader: 'babel-loader',
query: {
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',
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
entry: './index',
output: {
path: path.resolve(__dirname, 'public'),
filename: 'bundle.js',
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
comments: false,
mangle: false,
beautify: true,
}),
],
module: {
rules: [
{
test: /\.jsx$/, // regexp for JSX files
exclude: /node_modules/,
use: [ // order: from bottom to top
{
loader: 'babel-loader',
options: {
plugins: [ // order: from top to bottom
// 'transform-decorators-legacy', // @withStyles, @withTheme
'transform-class-properties', // static defaultProps
'transform-flow-strip-types',
],
presets: [ // order: from bottom to top
'env',
'react',
'stage-0',
],
},
},
// 'eslint-loader', // show errors not only in the editor, but also in the console
],
},
{
test: /font-awesome\.css$/,
use: [
'style-loader',
'css-loader',
path.resolve(__dirname, './fa-only-woff-loader.js'),
],
},
{
test: /\.woff2?$/, // font-awesome icons
use: 'url-loader',
},
],
},
};

View file

@ -17,8 +17,8 @@
package dashboard
//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/...
//go:generate gofmt -s -w .
import (
"fmt"
@ -42,7 +42,7 @@ const (
trafficSampleLimit = 200 // Maximum number of traffic data samples
)
var nextId uint32 // Next connection id
var nextID uint32 // Next connection id
// Dashboard contains the dashboard internals.
type Dashboard struct {
@ -50,46 +50,30 @@ type Dashboard struct {
listener net.Listener
conns map[uint32]*client // Currently live websocket connections
charts charts // The collected data samples to plot
lock sync.RWMutex // Lock protecting the dashboard's internals
charts *HomeMessage
lock sync.RWMutex // Lock protecting the dashboard's internals
quit chan chan error // Channel used for graceful exit
wg sync.WaitGroup
}
// message embraces the data samples of a client message.
type message struct {
History *charts `json:"history,omitempty"` // Past data samples
Memory *chartEntry `json:"memory,omitempty"` // One memory sample
Traffic *chartEntry `json:"traffic,omitempty"` // One traffic sample
Log string `json:"log,omitempty"` // One log
}
// client represents active websocket connection with a remote browser.
type client struct {
conn *websocket.Conn // Particular live websocket connection
msg chan message // Message queue for the update messages
msg chan Message // Message queue for the update messages
logger log.Logger // Logger for the particular live websocket connection
}
// charts contains the collected data samples.
type charts struct {
Memory []*chartEntry `json:"memorySamples,omitempty"`
Traffic []*chartEntry `json:"trafficSamples,omitempty"`
}
// chartEntry represents one data sample
type chartEntry struct {
Time time.Time `json:"time,omitempty"`
Value float64 `json:"value,omitempty"`
}
// New creates a new dashboard instance with the given configuration.
func New(config *Config) (*Dashboard, error) {
return &Dashboard{
conns: make(map[uint32]*client),
config: config,
quit: make(chan chan error),
charts: &HomeMessage{
Memory: &Chart{},
Traffic: &Chart{},
},
}, nil
}
@ -185,13 +169,13 @@ func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
// apiHandler handles requests for the dashboard.
func (db *Dashboard) apiHandler(conn *websocket.Conn) {
id := atomic.AddUint32(&nextId, 1)
id := atomic.AddUint32(&nextID, 1)
client := &client{
conn: conn,
msg: make(chan message, 128),
msg: make(chan Message, 128),
logger: log.New("id", id),
}
done := make(chan struct{}) // Buffered channel as sender may exit early
done := make(chan struct{})
// Start listening for messages to send.
db.wg.Add(1)
@ -212,8 +196,15 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
}
}()
// Send the past data.
client.msg <- message{
History: &db.charts,
client.msg <- Message{
Home: &HomeMessage{
Memory: &Chart{
History: db.charts.Memory.History,
},
Traffic: &Chart{
History: db.charts.Traffic.History,
},
},
}
// Start tracking the connection and drop at connection loss.
db.lock.Lock()
@ -247,29 +238,34 @@ func (db *Dashboard) collectData() {
inboundTraffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
now := time.Now()
memory := &chartEntry{
memory := &ChartEntry{
Time: now,
Value: memoryInUse,
}
traffic := &chartEntry{
traffic := &ChartEntry{
Time: now,
Value: inboundTraffic,
}
// Remove the first elements in case the samples' amount exceeds the limit.
first := 0
if len(db.charts.Memory) == memorySampleLimit {
if len(db.charts.Memory.History) == memorySampleLimit {
first = 1
}
db.charts.Memory = append(db.charts.Memory[first:], memory)
db.charts.Memory.History = append(db.charts.Memory.History[first:], memory)
first = 0
if len(db.charts.Traffic) == trafficSampleLimit {
if len(db.charts.Traffic.History) == trafficSampleLimit {
first = 1
}
db.charts.Traffic = append(db.charts.Traffic[first:], traffic)
db.charts.Traffic.History = append(db.charts.Traffic.History[first:], traffic)
db.sendToAll(&message{
Memory: memory,
Traffic: traffic,
db.sendToAll(&Message{
Home: &HomeMessage{
Memory: &Chart{
New: memory,
},
Traffic: &Chart{
New: traffic,
},
},
})
}
}
@ -287,8 +283,10 @@ func (db *Dashboard) collectLogs() {
errc <- nil
return
case <-time.After(db.config.Refresh / 2):
db.sendToAll(&message{
Log: fmt.Sprint(id, ": This is a fake log."),
db.sendToAll(&Message{
Logs: &LogsMessage{
Log: fmt.Sprintf("%-4d: This is a fake log.", id),
},
})
id++
}
@ -296,7 +294,7 @@ func (db *Dashboard) collectLogs() {
}
// sendToAll sends the given message to the active dashboards.
func (db *Dashboard) sendToAll(msg *message) {
func (db *Dashboard) sendToAll(msg *Message) {
db.lock.Lock()
for _, c := range db.conns {
select {

63
dashboard/message.go Normal file
View file

@ -0,0 +1,63 @@
// 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/>.
package dashboard
import "time"
type Message struct {
Home *HomeMessage `json:"home,omitempty"`
Chain *ChainMessage `json:"chain,omitempty"`
TxPool *TxPoolMessage `json:"txpool,omitempty"`
Network *NetworkMessage `json:"network,omitempty"`
System *SystemMessage `json:"system,omitempty"`
Logs *LogsMessage `json:"logs,omitempty"`
}
type HomeMessage struct {
Memory *Chart `json:"memory,omitempty"`
Traffic *Chart `json:"traffic,omitempty"`
}
type Chart struct {
History []*ChartEntry `json:"history,omitempty"`
New *ChartEntry `json:"new,omitempty"`
}
type ChartEntry struct {
Time time.Time `json:"time,omitempty"`
Value float64 `json:"value,omitempty"`
}
type ChainMessage struct {
/* TODO (kurkomisi) */
}
type TxPoolMessage struct {
/* TODO (kurkomisi) */
}
type NetworkMessage struct {
/* TODO (kurkomisi) */
}
type SystemMessage struct {
/* TODO (kurkomisi) */
}
type LogsMessage struct {
Log string `json:"log,omitempty"`
}