diff options
author | Kurkó Mihály <kurkomisi@users.noreply.github.com> | 2018-01-15 17:20:00 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2018-01-15 17:20:00 +0800 |
commit | 938cf4528ab5acbb6013be79a0548956713807a8 (patch) | |
tree | f7782996f31b39539d148d5daeea9171415db87c /dashboard/assets | |
parent | 81ad8f665d3e7598273958d557c531f800eca50f (diff) | |
download | dexon-938cf4528ab5acbb6013be79a0548956713807a8.tar.gz dexon-938cf4528ab5acbb6013be79a0548956713807a8.tar.zst dexon-938cf4528ab5acbb6013be79a0548956713807a8.zip |
dashboard: deep state update, version in footer (#15837)
* dashboard: footer, deep state update
* dashboard: resolve asset path
* dashboard: remove bundle.js
* dashboard: prevent state update on every reconnection
* dashboard: fix linter issue
* dashboard, cmd: minor UI fix, include commit hash
* remove geth binary
* dashboard: gitCommit renamed to commit
* dashboard: move the geth version to the right, make commit optional
* dashboard: commit limited to 7 characters
* dashboard: limit commit length on client side
* dashboard: run go generate
Diffstat (limited to 'dashboard/assets')
-rw-r--r-- | dashboard/assets/components/Common.jsx | 28 | ||||
-rw-r--r-- | dashboard/assets/components/Dashboard.jsx | 193 | ||||
-rw-r--r-- | dashboard/assets/components/Footer.jsx | 80 | ||||
-rw-r--r-- | dashboard/assets/components/Home.jsx | 13 | ||||
-rw-r--r-- | dashboard/assets/dashboard.html (renamed from dashboard/assets/public/dashboard.html) | 0 | ||||
-rw-r--r-- | dashboard/assets/package.json | 14 | ||||
-rw-r--r-- | dashboard/assets/types/content.jsx | 41 | ||||
-rw-r--r-- | dashboard/assets/types/message.jsx | 61 | ||||
-rw-r--r-- | dashboard/assets/webpack.config.js | 2 |
9 files changed, 234 insertions, 198 deletions
diff --git a/dashboard/assets/components/Common.jsx b/dashboard/assets/components/Common.jsx index d8723830e..256a3e661 100644 --- a/dashboard/assets/components/Common.jsx +++ b/dashboard/assets/components/Common.jsx @@ -62,32 +62,4 @@ export type MenuProp = {|...ProvidedMenuProp, id: string|}; // 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 DURATION = 200; - -export const LENS: Map<string, string> = new Map([ - 'content', - ...menuSkeletons.map(({id}) => id), - ...sampleSkeletons.map(({id}) => id), -].map(lens => [lens, lens])); diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index b60736d8c..036dd050b 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -19,37 +19,99 @@ import React, {Component} from 'react'; import withStyles from 'material-ui/styles/withStyles'; -import {lensPath, view, set} from 'ramda'; import Header from './Header'; import Body from './Body'; -import {MENU, SAMPLE} from './Common'; -import type {Message, HomeMessage, LogsMessage, Chart} from '../types/message'; +import Footer from './Footer'; +import {MENU} from './Common'; import type {Content} from '../types/content'; -// 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. +// deepUpdate updates an object corresponding to the given update data, which has +// the shape of the same structure as the original object. updater also has the same +// structure, except that it contains functions where the original data needs to be +// updated. These functions are used to handle the update. // -// 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 - ); +// Since the messages have the same shape as the state content, this approach allows +// the generalization of the message handling. The only necessary thing is to set a +// handler function for every path of the state in order to maximize the flexibility +// of the update. +const deepUpdate = (prev: Object, update: Object, updater: Object) => { + if (typeof update === 'undefined') { + // TODO (kurkomisi): originally this was deep copy, investigate it. + return prev; + } + if (typeof updater === 'function') { + return updater(prev, update); + } + const updated = {}; + Object.keys(prev).forEach((key) => { + updated[key] = deepUpdate(prev[key], update[key], updater[key]); + }); + + return updated; +}; + +// shouldUpdate returns the structure of a message. It is used to prevent unnecessary render +// method triggerings. In the affected component's shouldComponentUpdate method it can be checked +// whether the involved data was changed or not by checking the message structure. +// +// We could return the message itself too, but it's safer not to give access to it. +const shouldUpdate = (msg: Object, updater: Object) => { + const su = {}; + Object.keys(msg).forEach((key) => { + su[key] = typeof updater[key] !== 'function' ? shouldUpdate(msg[key], updater[key]) : true; + }); + + return su; }; -// 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. + +// appender is a state update generalization function, which appends the update data +// to the existing data. limit defines the maximum allowed size of the created array. +const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit); + +// replacer is a state update generalization function, which replaces the original data. +const replacer = <T>(prev: T, update: T) => update; + +// defaultContent is the initial value of the state content. +const defaultContent: Content = { + general: { + version: null, + commit: null, + }, + home: { + memory: [], + traffic: [], + }, + chain: {}, + txpool: {}, + network: {}, + system: {}, + logs: { + log: [], + }, +}; + +// updaters contains the state update generalization functions for each path of the state. +// TODO (kurkomisi): Define a tricky type which embraces the content and the handlers. +const updaters = { + general: { + version: replacer, + commit: replacer, + }, + home: { + memory: appender(200), + traffic: appender(200), + }, + chain: null, + txpool: null, + network: null, + system: null, + logs: { + log: appender(200), + }, +}; + +// styles returns the styles for the Dashboard component. const styles = theme => ({ dashboard: { display: 'flex', @@ -61,15 +123,18 @@ const styles = theme => ({ 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 + content: Content, // the visualized data + shouldUpdate: Object // 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> { @@ -78,8 +143,8 @@ class Dashboard extends Component<Props, State> { this.state = { active: MENU.get('home').id, sideBar: true, - content: {home: {memory: [], traffic: []}, logs: {log: []}}, - shouldUpdate: new Set(), + content: defaultContent, + shouldUpdate: {}, }; } @@ -91,13 +156,14 @@ 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 = () => { - this.setState({ - content: {home: {memory: [], traffic: []}, logs: {log: []}}, - }); const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host}/api`); + server.onopen = () => { + this.setState({content: defaultContent, shouldUpdate: {}}); + }; server.onmessage = (event) => { - const msg: Message = JSON.parse(event.data); + const msg: $Shape<Content> = JSON.parse(event.data); if (!msg) { + console.error(`Incoming message is ${msg}`); return; } this.update(msg); @@ -107,56 +173,12 @@ class Dashboard extends Component<Props, State> { }; }; - // 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; - }; - - // 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; - }); - }; - - // 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: Message) => { - if (msg.home) { - this.handleHome(msg.home); - } - if (msg.logs) { - this.handleLogs(msg.logs); - } + // update updates the content corresponding to the incoming message. + update = (msg: $Shape<Content>) => { + this.setState(prevState => ({ + content: deepUpdate(prevState.content, msg, updaters), + shouldUpdate: shouldUpdate(msg, updaters), + })); }; // changeContent sets the active label, which is used at the content rendering. @@ -191,6 +213,13 @@ class Dashboard extends Component<Props, State> { content={this.state.content} shouldUpdate={this.state.shouldUpdate} /> + <Footer + opened={this.state.sideBar} + openSideBar={this.openSideBar} + closeSideBar={this.closeSideBar} + general={this.state.content.general} + shouldUpdate={this.state.shouldUpdate} + /> </div> ); } diff --git a/dashboard/assets/components/Footer.jsx b/dashboard/assets/components/Footer.jsx new file mode 100644 index 000000000..7130b4e4e --- /dev/null +++ b/dashboard/assets/components/Footer.jsx @@ -0,0 +1,80 @@ +// @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 AppBar from 'material-ui/AppBar'; +import Toolbar from 'material-ui/Toolbar'; +import Typography from 'material-ui/Typography'; + +import type {General} from '../types/content'; + +// styles contains styles for the Header component. +const styles = theme => ({ + footer: { + 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, + display: 'flex', + justifyContent: 'flex-end', + }, + light: { + color: 'rgba(255, 255, 255, 0.54)', + }, +}); +export type Props = { + general: General, + classes: Object, +}; +// TODO (kurkomisi): If the structure is appropriate, make an abstraction of the common parts with the Header. +// Footer renders the header of the dashboard. +class Footer extends Component<Props> { + shouldComponentUpdate(nextProps) { + return typeof nextProps.shouldUpdate.logs !== 'undefined'; + } + + info = (about: string, data: string) => ( + <Typography type="caption" color="inherit"> + <span className={this.props.classes.light}>{about}</span> {data} + </Typography> + ); + + render() { + const {classes, general} = this.props; // The classes property is injected by withStyles(). + const geth = general.version ? this.info('Geth', general.version) : null; + const commit = general.commit ? this.info('Commit', general.commit.substring(0, 7)) : null; + + return ( + <AppBar position="static" className={classes.footer}> + <Toolbar className={classes.toolbar}> + <div> + {geth} + {commit} + </div> + </Toolbar> + </AppBar> + ); + } +} + +export default withStyles(styles)(Footer); diff --git a/dashboard/assets/components/Home.jsx b/dashboard/assets/components/Home.jsx index d3e1004f9..f9fd7bf46 100644 --- a/dashboard/assets/components/Home.jsx +++ b/dashboard/assets/components/Home.jsx @@ -22,13 +22,13 @@ import withTheme from 'material-ui/styles/withTheme'; import {LineChart, AreaChart, Area, YAxis, CartesianGrid, Line} from 'recharts'; import ChartGrid from './ChartGrid'; -import type {ChartEntry} from '../types/message'; +import type {ChartEntry} from '../types/content'; export type Props = { theme: Object, memory: Array<ChartEntry>, traffic: Array<ChartEntry>, - shouldUpdate: Object, + shouldUpdate: Object, }; // Home renders the home content. class Home extends Component<Props> { @@ -40,11 +40,16 @@ class Home extends Component<Props> { } shouldComponentUpdate(nextProps) { - return nextProps.shouldUpdate.has('memory') || nextProps.shouldUpdate.has('traffic'); + return typeof nextProps.shouldUpdate.home !== 'undefined'; } + memoryColor: Object; + trafficColor: Object; + render() { - const {memory, traffic} = this.props; + let {memory, traffic} = this.props; + memory = memory.map(({value}) => (value || 0)); + traffic = traffic.map(({value}) => (value || 0)); return ( <ChartGrid spacing={24}> diff --git a/dashboard/assets/public/dashboard.html b/dashboard/assets/dashboard.html index 2491bf1ea..2491bf1ea 100644 --- a/dashboard/assets/public/dashboard.html +++ b/dashboard/assets/dashboard.html diff --git a/dashboard/assets/package.json b/dashboard/assets/package.json index 5bbfc185c..139dede74 100644 --- a/dashboard/assets/package.json +++ b/dashboard/assets/package.json @@ -1,7 +1,7 @@ { "dependencies": { "babel-core": "^6.26.0", - "babel-eslint": "^8.0.3", + "babel-eslint": "^8.1.2", "babel-loader": "^7.1.2", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", @@ -12,28 +12,28 @@ "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", + "css-loader": "^0.28.8", + "eslint": "^4.15.0", "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", + "eslint-plugin-flowtype": "^2.41.0", "file-loader": "^1.1.6", - "flow-bin": "^0.61.0", + "flow-bin": "^0.63.1", "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", + "recharts": "^1.0.0-beta.7", "style-loader": "^0.19.1", + "typeface-roboto": "^0.0.50", "url": "^0.11.0", "url-loader": "^0.6.2", "webpack": "^3.10.0" diff --git a/dashboard/assets/types/content.jsx b/dashboard/assets/types/content.jsx index f8a2b1e50..5e59b002c 100644 --- a/dashboard/assets/types/content.jsx +++ b/dashboard/assets/types/content.jsx @@ -16,38 +16,49 @@ // 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, + general: General, + home: Home, + chain: Chain, + txpool: TxPool, + network: Network, + system: System, + logs: Logs, +}; + +export type General = { + version: ?string, + commit: ?string, }; export type Home = { - memory: Array<ChartEntry>, - traffic: Array<ChartEntry>, + memory: ChartEntries, + traffic: ChartEntries, +}; + +export type ChartEntries = Array<ChartEntry>; + +export type ChartEntry = { + time: Date, + value: number, }; export type Chain = { - /* TODO (kurkomisi) */ + /* TODO (kurkomisi) */ }; export type TxPool = { - /* TODO (kurkomisi) */ + /* TODO (kurkomisi) */ }; export type Network = { - /* TODO (kurkomisi) */ + /* TODO (kurkomisi) */ }; export type System = { - /* TODO (kurkomisi) */ + /* TODO (kurkomisi) */ }; export type Logs = { - log: Array<string>, + log: Array<string>, }; diff --git a/dashboard/assets/types/message.jsx b/dashboard/assets/types/message.jsx deleted file mode 100644 index a806196ca..000000000 --- a/dashboard/assets/types/message.jsx +++ /dev/null @@ -1,61 +0,0 @@ -// @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, -}; diff --git a/dashboard/assets/webpack.config.js b/dashboard/assets/webpack.config.js index cf92e6c97..d90c4fabd 100644 --- a/dashboard/assets/webpack.config.js +++ b/dashboard/assets/webpack.config.js @@ -23,7 +23,7 @@ module.exports = { }, entry: './index', output: { - path: path.resolve(__dirname, 'public'), + path: path.resolve(__dirname, ''), filename: 'bundle.js', }, plugins: [ |