NodeInfo.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import React, { Component } from 'react';
  2. import { connect } from 'react-redux';
  3. import moment from 'moment';
  4. import PieChart from 'react-minimal-pie-chart';
  5. class NodeInfo extends Component {
  6. constructor(props) {
  7. super(props);
  8. this.state = {
  9. showSubmenu: false,
  10. peerCount: 0,
  11. ticks: 0,
  12. lightClasses: ''
  13. };
  14. }
  15. componentDidMount() {
  16. // NOTE: this goal of this component is to give status updates at
  17. // least once per second. The `tick` function ensures that.
  18. this.interval = setInterval(() => {
  19. this.tick();
  20. }, 50);
  21. }
  22. componentDidUpdate(prevProps, prevState) {
  23. // if new block arrived, add animation to light
  24. if (this.isNewBlock(prevProps, this.props)) {
  25. const lightClasses =
  26. prevProps.active === 'remote'
  27. ? 'pulse-light__orange'
  28. : 'pulse-light__green';
  29. this.setState({ lightClasses }, () => {
  30. setTimeout(() => {
  31. this.setState({ lightClasses: '' });
  32. }, 2000);
  33. });
  34. }
  35. }
  36. isNewBlock(prevProps, newProps) {
  37. if (prevProps.active === 'remote') {
  38. return prevProps.remote.blockNumber !== newProps.remote.blockNumber;
  39. } else {
  40. return prevProps.local.blockNumber !== newProps.local.blockNumber;
  41. }
  42. }
  43. componentWillUnmount() {
  44. clearInterval(this.interval);
  45. }
  46. tick() {
  47. if (this.state.ticks % 20 == 0) {
  48. // only do it every second
  49. web3.eth.net.getPeerCount().then(peerCount => {
  50. this.setState({ peerCount });
  51. });
  52. }
  53. this.setState({ ticks: this.state.ticks + 1 });
  54. }
  55. renderRemoteStats() {
  56. // Hide remote stats if local node is synced
  57. if (this.props.active !== 'remote') {
  58. return null;
  59. }
  60. const formattedBlockNumber = numeral(this.props.remote.blockNumber).format(
  61. '0,0'
  62. );
  63. const remoteTimestamp = moment.unix(this.props.remote.timestamp);
  64. const diff = moment().diff(remoteTimestamp, 'seconds');
  65. if (this.props.remote.blockNumber < 1000) {
  66. // Still loading initial remote results
  67. return (
  68. <div id="remote-stats" className="node-info__section">
  69. <div className="node-info__node-title orange">
  70. <strong>Remote</strong> Node
  71. </div>
  72. <div>
  73. <div className="remote-loading row-icon">
  74. <i className="icon icon-energy" />
  75. {i18n.t('mist.nodeInfo.connecting')}
  76. </div>
  77. </div>
  78. </div>
  79. );
  80. } else {
  81. return (
  82. <div id="remote-stats" className="node-info__section">
  83. <div className="node-info__node-title orange">
  84. <strong>Remote</strong> Node
  85. <span className="node-info__pill">
  86. {i18n.t('mist.nodeInfo.active')}
  87. </span>
  88. </div>
  89. <div className="block-number row-icon">
  90. <i className="icon icon-layers" /> {formattedBlockNumber}
  91. </div>
  92. <div
  93. className={
  94. diff > 60 ? 'block-diff row-icon red' : 'block-diff row-icon'
  95. }
  96. >
  97. {
  98. // TODO: make this i8n compatible
  99. }
  100. <i className="icon icon-clock" />
  101. {diff < 120
  102. ? diff + ' seconds'
  103. : Math.floor(diff / 60) + ' minutes'}
  104. </div>
  105. </div>
  106. );
  107. }
  108. }
  109. localStatsFindingPeers() {
  110. return (
  111. <div>
  112. <div className="looking-for-peers row-icon">
  113. <i className="icon icon-share" />
  114. {i18n.t('mist.nodeInfo.lookingForPeers')}
  115. </div>
  116. </div>
  117. );
  118. }
  119. localStatsStartSync() {
  120. return (
  121. <div>
  122. <div className="peer-count row-icon">
  123. <i className="icon icon-users" />
  124. {` ${this.state.peerCount} ${i18n.t('mist.nodeInfo.peers')}`}
  125. </div>
  126. <div className="sync-starting row-icon">
  127. <i className="icon icon-energy" />
  128. {i18n.t('mist.nodeInfo.syncStarting')}
  129. </div>
  130. </div>
  131. );
  132. }
  133. localStatsSyncProgress() {
  134. const { highestBlock, currentBlock, startingBlock } = this.props.local.sync;
  135. let displayBlock =
  136. this.props.local.sync.displayBlock || this.props.local.sync.startingBlock;
  137. displayBlock += (currentBlock - displayBlock) / 20;
  138. let formattedDisplayBlock = numeral(displayBlock).format('0,0');
  139. this.props.local.sync.displayBlock = displayBlock;
  140. const progress =
  141. ((displayBlock - startingBlock) / (highestBlock - startingBlock)) * 100;
  142. return (
  143. <div>
  144. <div className="block-number row-icon">
  145. <i className="icon icon-layers" />
  146. {formattedDisplayBlock}
  147. </div>
  148. <div className="peer-count row-icon">
  149. <i className="icon icon-users" />
  150. {` ${this.state.peerCount} ${i18n.t('mist.nodeInfo.peers')}`}
  151. </div>
  152. <div className="sync-progress row-icon">
  153. <i className="icon icon-cloud-download" />
  154. <progress max="100" value={progress || 0} />
  155. </div>
  156. </div>
  157. );
  158. }
  159. localStatsSynced() {
  160. const { blockNumber, timestamp, syncMode } = this.props.local;
  161. const formattedBlockNumber = numeral(blockNumber).format('0,0');
  162. const timeSince = moment(timestamp, 'X');
  163. const diff = moment().diff(timeSince, 'seconds');
  164. return (
  165. <div>
  166. <div
  167. className="block-number row-icon"
  168. title={i18n.t('mist.nodeInfo.blockNumber')}
  169. >
  170. <i className="icon icon-layers" /> {formattedBlockNumber}
  171. </div>
  172. {this.props.network !== 'private' && (
  173. <div className="peer-count row-icon">
  174. <i className="icon icon-users" />
  175. {` ${this.state.peerCount} ${i18n.t('mist.nodeInfo.peers')}`}
  176. </div>
  177. )}
  178. <div
  179. className="block-diff row-icon"
  180. title={i18n.t('mist.nodeInfo.timeSinceBlock')}
  181. >
  182. <i className="icon icon-clock" /> {diff} seconds
  183. </div>
  184. </div>
  185. );
  186. }
  187. renderLocalStats() {
  188. const { syncMode } = this.props.local;
  189. const { currentBlock } = this.props.local.sync;
  190. let syncText;
  191. if (syncMode) {
  192. syncText = syncMode === 'nosync' ? `sync off` : `${syncMode} sync`;
  193. }
  194. let localStats;
  195. // TODO: potentially refactor local node status into Redux;
  196. // possible states: findingPeers, starting, synced, synced, disabled/nosync
  197. // Determine 'status' of local node, then show appropriate lens on sync data
  198. if (syncMode === 'nosync') {
  199. // Case: no local node
  200. return null;
  201. } else if (this.props.active === 'local') {
  202. // Case: already synced up
  203. localStats = this.localStatsSynced();
  204. } else if (this.props.active === 'remote') {
  205. // Case: not yet synced up
  206. if (currentBlock === 0) {
  207. // Case: no results from syncing
  208. if (this.state.peerCount === 0) {
  209. // Case: no peers yet
  210. localStats = this.localStatsFindingPeers();
  211. } else {
  212. // Case: connected to peers, but no blocks yet
  213. localStats = this.localStatsStartSync();
  214. }
  215. } else {
  216. // Case: show progress
  217. localStats = this.localStatsSyncProgress();
  218. }
  219. }
  220. return (
  221. <div id="local-stats" className="node-info__section">
  222. <div className="node-info__node-title local">
  223. <strong>{i18n.t('mist.nodeInfo.local')}</strong>{' '}
  224. {i18n.t('mist.nodeInfo.node')}
  225. {syncText && <span className="node-info__pill">{syncText}</span>}
  226. </div>
  227. {localStats}
  228. </div>
  229. );
  230. }
  231. renderStatusLight() {
  232. const { active, network, remote } = this.props;
  233. const timeSince = moment(remote.timestamp, 'X');
  234. const diff = moment().diff(timeSince, 'seconds');
  235. let dotColor = network == 'main' ? '#7ed321' : '#00aafa';
  236. const { highestBlock, currentBlock, startingBlock } = this.props.local.sync;
  237. const progress =
  238. ((currentBlock - startingBlock) / (highestBlock - startingBlock)) * 100;
  239. return (
  240. <div className="pie-container">
  241. <div
  242. id="node-info__light"
  243. className={this.state.lightClasses}
  244. style={{
  245. backgroundColor:
  246. diff > 60 ? 'red' : active === 'remote' ? 'orange' : dotColor
  247. }}
  248. />
  249. {active === 'remote' &&
  250. currentBlock !== 0 && (
  251. <PieChart
  252. startAngle={-90}
  253. style={{
  254. position: 'absolute',
  255. top: 22,
  256. left: 0,
  257. zIndex: 2,
  258. height: 16
  259. }}
  260. data={[
  261. { value: progress || 0, key: 1, color: dotColor },
  262. { value: 100 - (progress || 1), key: 2, color: 'orange' }
  263. ]}
  264. />
  265. )}
  266. </div>
  267. );
  268. }
  269. render() {
  270. const { network } = this.props;
  271. let mainClass = network == 'main' ? 'node-mainnet' : 'node-testnet';
  272. if (this.state.sticky) mainClass += ' sticky';
  273. return (
  274. <div
  275. id="node-info"
  276. className={mainClass}
  277. onMouseUp={() => this.setState({ sticky: !this.state.sticky })}
  278. onMouseEnter={() => this.setState({ showSubmenu: true })}
  279. onMouseLeave={() => this.setState({ showSubmenu: this.state.sticky })}
  280. >
  281. {this.renderStatusLight()}
  282. {this.state.showSubmenu && (
  283. <section className="node-info__submenu-container">
  284. <section>
  285. <div className="node-info__section">
  286. <div className="node-info__network-title">{network}</div>
  287. <div className="node-info__subtitle">
  288. {network !== 'main' && i18n.t('mist.nodeInfo.testNetwork')}
  289. {network === 'main' && i18n.t('mist.nodeInfo.network')}
  290. </div>
  291. </div>
  292. {this.renderRemoteStats()}
  293. {this.renderLocalStats()}
  294. </section>
  295. </section>
  296. )}
  297. </div>
  298. );
  299. }
  300. }
  301. function mapStateToProps(state) {
  302. return {
  303. active: state.nodes.active,
  304. network: state.nodes.network,
  305. remote: state.nodes.remote,
  306. local: state.nodes.local
  307. };
  308. }
  309. export default connect(mapStateToProps)(NodeInfo);