Dashboard.jsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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 {hot} from 'react-hot-loader';
  19. import withStyles from '@material-ui/core/styles/withStyles';
  20. import Header from 'Header';
  21. import Body from 'Body';
  22. import {inserter as logInserter, SAME} from 'Logs';
  23. import {inserter as peerInserter} from 'Network';
  24. import {MENU} from '../common';
  25. import type {Content} from '../types/content';
  26. // deepUpdate updates an object corresponding to the given update data, which has
  27. // the shape of the same structure as the original object. updater also has the same
  28. // structure, except that it contains functions where the original data needs to be
  29. // updated. These functions are used to handle the update.
  30. //
  31. // Since the messages have the same shape as the state content, this approach allows
  32. // the generalization of the message handling. The only necessary thing is to set a
  33. // handler function for every path of the state in order to maximize the flexibility
  34. // of the update.
  35. const deepUpdate = (updater: Object, update: Object, prev: Object): $Shape<Content> => {
  36. if (typeof update === 'undefined') {
  37. return prev;
  38. }
  39. if (typeof updater === 'function') {
  40. return updater(update, prev);
  41. }
  42. const updated = {};
  43. Object.keys(prev).forEach((key) => {
  44. updated[key] = deepUpdate(updater[key], update[key], prev[key]);
  45. });
  46. return updated;
  47. };
  48. // shouldUpdate returns the structure of a message. It is used to prevent unnecessary render
  49. // method triggerings. In the affected component's shouldComponentUpdate method it can be checked
  50. // whether the involved data was changed or not by checking the message structure.
  51. //
  52. // We could return the message itself too, but it's safer not to give access to it.
  53. const shouldUpdate = (updater: Object, msg: Object) => {
  54. const su = {};
  55. Object.keys(msg).forEach((key) => {
  56. su[key] = typeof updater[key] !== 'function' ? shouldUpdate(updater[key], msg[key]) : true;
  57. });
  58. return su;
  59. };
  60. // replacer is a state updater function, which replaces the original data.
  61. const replacer = <T>(update: T) => update;
  62. // appender is a state updater function, which appends the update data to the
  63. // existing data. limit defines the maximum allowed size of the created array,
  64. // mapper maps the update data.
  65. const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, prev: Array<T>) => [
  66. ...prev,
  67. ...update.map(sample => mapper(sample)),
  68. ].slice(-limit);
  69. // defaultContent returns the initial value of the state content. Needs to be a function in order to
  70. // instantiate the object again, because it is used by the state, and isn't automatically cleaned
  71. // when a new connection is established. The state is mutated during the update in order to avoid
  72. // the execution of unnecessary operations (e.g. copy of the log array).
  73. const defaultContent: () => Content = () => ({
  74. general: {
  75. version: null,
  76. commit: null,
  77. },
  78. home: {},
  79. chain: {},
  80. txpool: {},
  81. network: {
  82. peers: {
  83. bundles: {},
  84. },
  85. diff: [],
  86. },
  87. system: {
  88. activeMemory: [],
  89. virtualMemory: [],
  90. networkIngress: [],
  91. networkEgress: [],
  92. processCPU: [],
  93. systemCPU: [],
  94. diskRead: [],
  95. diskWrite: [],
  96. },
  97. logs: {
  98. chunks: [],
  99. endTop: false,
  100. endBottom: true,
  101. topChanged: SAME,
  102. bottomChanged: SAME,
  103. },
  104. });
  105. // updaters contains the state updater functions for each path of the state.
  106. //
  107. // TODO (kurkomisi): Define a tricky type which embraces the content and the updaters.
  108. const updaters = {
  109. general: {
  110. version: replacer,
  111. commit: replacer,
  112. },
  113. home: null,
  114. chain: null,
  115. txpool: null,
  116. network: peerInserter(200),
  117. system: {
  118. activeMemory: appender(200),
  119. virtualMemory: appender(200),
  120. networkIngress: appender(200),
  121. networkEgress: appender(200),
  122. processCPU: appender(200),
  123. systemCPU: appender(200),
  124. diskRead: appender(200),
  125. diskWrite: appender(200),
  126. },
  127. logs: logInserter(5),
  128. };
  129. // styles contains the constant styles of the component.
  130. const styles = {
  131. dashboard: {
  132. display: 'flex',
  133. flexFlow: 'column',
  134. width: '100%',
  135. height: '100%',
  136. zIndex: 1,
  137. overflow: 'hidden',
  138. },
  139. };
  140. // themeStyles returns the styles generated from the theme for the component.
  141. const themeStyles: Object = (theme: Object) => ({
  142. dashboard: {
  143. background: theme.palette.background.default,
  144. },
  145. });
  146. export type Props = {
  147. classes: Object, // injected by withStyles()
  148. };
  149. type State = {
  150. active: string, // active menu
  151. sideBar: boolean, // true if the sidebar is opened
  152. content: Content, // the visualized data
  153. shouldUpdate: Object, // labels for the components, which need to re-render based on the incoming message
  154. server: ?WebSocket,
  155. };
  156. // Dashboard is the main component, which renders the whole page, makes connection with the server and
  157. // listens for messages. When there is an incoming message, updates the page's content correspondingly.
  158. class Dashboard extends Component<Props, State> {
  159. constructor(props: Props) {
  160. super(props);
  161. this.state = {
  162. active: MENU.get('home').id,
  163. sideBar: true,
  164. content: defaultContent(),
  165. shouldUpdate: {},
  166. server: null,
  167. };
  168. }
  169. // componentDidMount initiates the establishment of the first websocket connection after the component is rendered.
  170. componentDidMount() {
  171. this.reconnect();
  172. }
  173. // reconnect establishes a websocket connection with the server, listens for incoming messages
  174. // and tries to reconnect on connection loss.
  175. reconnect = () => {
  176. const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:8080';
  177. const server = new WebSocket(`${((window.location.protocol === 'https:') ? 'wss://' : 'ws://')}${host}/api`);
  178. server.onopen = () => {
  179. this.setState({content: defaultContent(), shouldUpdate: {}, server});
  180. };
  181. server.onmessage = (event) => {
  182. const msg: $Shape<Content> = JSON.parse(event.data);
  183. if (!msg) {
  184. console.error(`Incoming message is ${msg}`);
  185. return;
  186. }
  187. this.update(msg);
  188. };
  189. server.onclose = () => {
  190. this.setState({server: null});
  191. setTimeout(this.reconnect, 3000);
  192. };
  193. };
  194. // send sends a message to the server, which can be accessed only through this function for safety reasons.
  195. send = (msg: string) => {
  196. if (this.state.server != null) {
  197. this.state.server.send(msg);
  198. }
  199. };
  200. // update updates the content corresponding to the incoming message.
  201. update = (msg: $Shape<Content>) => {
  202. this.setState(prevState => ({
  203. content: deepUpdate(updaters, msg, prevState.content),
  204. shouldUpdate: shouldUpdate(updaters, msg),
  205. }));
  206. };
  207. // changeContent sets the active label, which is used at the content rendering.
  208. changeContent = (newActive: string) => {
  209. this.setState(prevState => (prevState.active !== newActive ? {active: newActive} : {}));
  210. };
  211. // switchSideBar opens or closes the sidebar's state.
  212. switchSideBar = () => {
  213. this.setState(prevState => ({sideBar: !prevState.sideBar}));
  214. };
  215. render() {
  216. return (
  217. <div className={this.props.classes.dashboard} style={styles.dashboard}>
  218. <Header
  219. switchSideBar={this.switchSideBar}
  220. />
  221. <Body
  222. opened={this.state.sideBar}
  223. changeContent={this.changeContent}
  224. active={this.state.active}
  225. content={this.state.content}
  226. shouldUpdate={this.state.shouldUpdate}
  227. send={this.send}
  228. />
  229. </div>
  230. );
  231. }
  232. }
  233. export default hot(module)(withStyles(themeStyles)(Dashboard));