From 938cf4528ab5acbb6013be79a0548956713807a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Mon, 15 Jan 2018 11:20:00 +0200 Subject: 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 --- dashboard/assets/components/Common.jsx | 28 ----- dashboard/assets/components/Dashboard.jsx | 193 +++++++++++++++++------------- dashboard/assets/components/Footer.jsx | 80 +++++++++++++ dashboard/assets/components/Home.jsx | 13 +- dashboard/assets/dashboard.html | 23 ++++ dashboard/assets/package.json | 14 +-- dashboard/assets/public/dashboard.html | 23 ---- dashboard/assets/types/content.jsx | 41 ++++--- dashboard/assets/types/message.jsx | 61 ---------- dashboard/assets/webpack.config.js | 2 +- 10 files changed, 257 insertions(+), 221 deletions(-) create mode 100644 dashboard/assets/components/Footer.jsx create mode 100644 dashboard/assets/dashboard.html delete mode 100644 dashboard/assets/public/dashboard.html delete mode 100644 dashboard/assets/types/message.jsx (limited to 'dashboard/assets') 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 = 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 = new Map(sampleSkeletons.map(({id, sample}) => ([id, {id, ...sample}]))); - export const DURATION = 200; - -export const LENS: Map = 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 = (limit: number) => (prev: Array, update: Array) => [...prev, ...update].slice(-limit); + +// replacer is a state update generalization function, which replaces the original data. +const replacer = (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, // the visualized data - shouldUpdate: Set // 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 { @@ -78,8 +143,8 @@ class Dashboard extends Component { 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 { // 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 = JSON.parse(event.data); if (!msg) { + console.error(`Incoming message is ${msg}`); return; } this.update(msg); @@ -107,56 +173,12 @@ class Dashboard extends Component { }; }; - // 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) => { + 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 { content={this.state.content} shouldUpdate={this.state.shouldUpdate} /> +