Dashboard.jsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. // @flow
  2. // Copyright 2017 The go-ethereum Authors
  3. // This file is part of the go-ethereum library.
  4. //
  5. // The go-ethereum library is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Lesser General Public License as published by
  7. // the Free Software Foundation, either version 3 of the License, or
  8. // (at your option) any later version.
  9. //
  10. // The go-ethereum library is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Lesser General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Lesser General Public License
  16. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
  17. import React, {Component} from 'react';
  18. import withStyles from 'material-ui/styles/withStyles';
  19. import Header from './Header';
  20. import Body from './Body';
  21. import {MENU} from '../common';
  22. import type {Content} from '../types/content';
  23. // deepUpdate updates an object corresponding to the given update data, which has
  24. // the shape of the same structure as the original object. updater also has the same
  25. // structure, except that it contains functions where the original data needs to be
  26. // updated. These functions are used to handle the update.
  27. //
  28. // Since the messages have the same shape as the state content, this approach allows
  29. // the generalization of the message handling. The only necessary thing is to set a
  30. // handler function for every path of the state in order to maximize the flexibility
  31. // of the update.
  32. const deepUpdate = (updater: Object, update: Object, prev: Object): $Shape<Content> => {
  33. if (typeof update === 'undefined') {
  34. // TODO (kurkomisi): originally this was deep copy, investigate it.
  35. return prev;
  36. }
  37. if (typeof updater === 'function') {
  38. return updater(update, prev);
  39. }
  40. const updated = {};
  41. Object.keys(prev).forEach((key) => {
  42. updated[key] = deepUpdate(updater[key], update[key], prev[key]);
  43. });
  44. return updated;
  45. };
  46. // shouldUpdate returns the structure of a message. It is used to prevent unnecessary render
  47. // method triggerings. In the affected component's shouldComponentUpdate method it can be checked
  48. // whether the involved data was changed or not by checking the message structure.
  49. //
  50. // We could return the message itself too, but it's safer not to give access to it.
  51. const shouldUpdate = (updater: Object, msg: Object) => {
  52. const su = {};
  53. Object.keys(msg).forEach((key) => {
  54. su[key] = typeof updater[key] !== 'function' ? shouldUpdate(updater[key], msg[key]) : true;
  55. });
  56. return su;
  57. };
  58. // replacer is a state updater function, which replaces the original data.
  59. const replacer = <T>(update: T) => update;
  60. // appender is a state updater function, which appends the update data to the
  61. // existing data. limit defines the maximum allowed size of the created array,
  62. // mapper maps the update data.
  63. const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, prev: Array<T>) => [
  64. ...prev,
  65. ...update.map(sample => mapper(sample)),
  66. ].slice(-limit);
  67. // defaultContent is the initial value of the state content.
  68. const defaultContent: Content = {
  69. general: {
  70. version: null,
  71. commit: null,
  72. },
  73. home: {},
  74. chain: {},
  75. txpool: {},
  76. network: {},
  77. system: {
  78. activeMemory: [],
  79. virtualMemory: [],
  80. networkIngress: [],
  81. networkEgress: [],
  82. processCPU: [],
  83. systemCPU: [],
  84. diskRead: [],
  85. diskWrite: [],
  86. },
  87. logs: {
  88. log: [],
  89. },
  90. };
  91. // updaters contains the state updater functions for each path of the state.
  92. //
  93. // TODO (kurkomisi): Define a tricky type which embraces the content and the updaters.
  94. const updaters = {
  95. general: {
  96. version: replacer,
  97. commit: replacer,
  98. },
  99. home: null,
  100. chain: null,
  101. txpool: null,
  102. network: null,
  103. system: {
  104. activeMemory: appender(200),
  105. virtualMemory: appender(200),
  106. networkIngress: appender(200),
  107. networkEgress: appender(200),
  108. processCPU: appender(200),
  109. systemCPU: appender(200),
  110. diskRead: appender(200),
  111. diskWrite: appender(200),
  112. },
  113. logs: {
  114. log: appender(200),
  115. },
  116. };
  117. // styles contains the constant styles of the component.
  118. const styles = {
  119. dashboard: {
  120. display: 'flex',
  121. flexFlow: 'column',
  122. width: '100%',
  123. height: '100%',
  124. zIndex: 1,
  125. overflow: 'hidden',
  126. },
  127. };
  128. // themeStyles returns the styles generated from the theme for the component.
  129. const themeStyles: Object = (theme: Object) => ({
  130. dashboard: {
  131. background: theme.palette.background.default,
  132. },
  133. });
  134. export type Props = {
  135. classes: Object, // injected by withStyles()
  136. };
  137. type State = {
  138. active: string, // active menu
  139. sideBar: boolean, // true if the sidebar is opened
  140. content: Content, // the visualized data
  141. shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message
  142. };
  143. // Dashboard is the main component, which renders the whole page, makes connection with the server and
  144. // listens for messages. When there is an incoming message, updates the page's content correspondingly.
  145. class Dashboard extends Component<Props, State> {
  146. constructor(props: Props) {
  147. super(props);
  148. this.state = {
  149. active: MENU.get('home').id,
  150. sideBar: true,
  151. content: defaultContent,
  152. shouldUpdate: {},
  153. };
  154. }
  155. // componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
  156. componentDidMount() {
  157. this.reconnect();
  158. }
  159. // reconnect establishes a websocket connection with the server, listens for incoming messages
  160. // and tries to reconnect on connection loss.
  161. reconnect = () => {
  162. // PROD is defined by webpack.
  163. const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${PROD ? window.location.host : 'localhost:8080'}/api`);
  164. server.onopen = () => {
  165. this.setState({content: defaultContent, shouldUpdate: {}});
  166. };
  167. server.onmessage = (event) => {
  168. const msg: $Shape<Content> = JSON.parse(event.data);
  169. if (!msg) {
  170. console.error(`Incoming message is ${msg}`);
  171. return;
  172. }
  173. this.update(msg);
  174. };
  175. server.onclose = () => {
  176. setTimeout(this.reconnect, 3000);
  177. };
  178. };
  179. // update updates the content corresponding to the incoming message.
  180. update = (msg: $Shape<Content>) => {
  181. this.setState(prevState => ({
  182. content: deepUpdate(updaters, msg, prevState.content),
  183. shouldUpdate: shouldUpdate(updaters, msg),
  184. }));
  185. };
  186. // changeContent sets the active label, which is used at the content rendering.
  187. changeContent = (newActive: string) => {
  188. this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
  189. };
  190. // switchSideBar opens or closes the sidebar's state.
  191. switchSideBar = () => {
  192. this.setState(prevState => ({sideBar: !prevState.sideBar}));
  193. };
  194. render() {
  195. return (
  196. <div className={this.props.classes.dashboard} style={styles.dashboard}>
  197. <Header
  198. switchSideBar={this.switchSideBar}
  199. />
  200. <Body
  201. opened={this.state.sideBar}
  202. changeContent={this.changeContent}
  203. active={this.state.active}
  204. content={this.state.content}
  205. shouldUpdate={this.state.shouldUpdate}
  206. />
  207. </div>
  208. );
  209. }
  210. }
  211. export default withStyles(themeStyles)(Dashboard);