diff options
Diffstat (limited to 'dashboard/assets/components')
-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 |
4 files changed, 200 insertions, 114 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}> |