Dashboard.jsx 8.2 KB

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