ethereum/mist

View on GitHub
interface/components/NodeInfo/index.js

Summary

Maintainability
D
1 day
Test Coverage
import React, { Component } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import StatusLight from './StatusLight';
import { setLocalPeerCount } from '../../actions.js';

class NodeInfo extends Component {
  constructor(props) {
    super(props);

    this.state = {
      showSubmenu: false,
      ticks: 0
    };
  }

  componentDidMount() {
    // NOTE: this component should give status updates at least once per second
    this.interval = setInterval(() => {
      this.tick();
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  tick() {
    web3.eth.net.getPeerCount().then(peerCount => {
      this.props.dispatch(setLocalPeerCount(peerCount));
    });

    this.setState({ ticks: this.state.ticks + 1 });
  }

  renderRemoteStats() {
    // Hide remote stats if local node is synced
    if (this.props.active !== 'remote') {
      return null;
    }

    const formattedBlockNumber = numeral(this.props.remote.blockNumber).format(
      '0,0'
    );
    const remoteTimestamp = moment.unix(this.props.remote.timestamp);
    const diff = moment().diff(remoteTimestamp, 'seconds');

    if (this.props.remote.blockNumber < 1000) {
      // Still loading initial remote results
      return (
        <div id="remote-stats" className="node-info__section">
          <div className="node-info__node-title orange">
            <strong>Remote</strong> Node
          </div>
          <div>
            <div className="remote-loading row-icon">
              <i className="icon icon-energy" />
              {i18n.t('mist.nodeInfo.connecting')}
            </div>
          </div>
        </div>
      );
    } else {
      return (
        <div id="remote-stats" className="node-info__section">
          <div className="node-info__node-title orange">
            <strong>Remote</strong> Node
            <span className="node-info__pill">
              {i18n.t('mist.nodeInfo.active')}
            </span>
          </div>
          <div className="block-number row-icon">
            <i className="icon icon-layers" /> {formattedBlockNumber}
          </div>
          {this.renderTimeSince(diff)}
        </div>
      );
    }
  }

  localStatsFindingPeers() {
    return (
      <div>
        <div className="looking-for-peers row-icon">
          <i className="icon icon-share" />
          {i18n.t('mist.nodeInfo.lookingForPeers')}
        </div>
      </div>
    );
  }

  localStatsStartSync() {
    return (
      <div>
        <div className="peer-count row-icon">
          <i className="icon icon-users" />
          {` ${this.props.local.peerCount} ${i18n.t('mist.nodeInfo.peers')}`}
        </div>
        <div className="sync-starting row-icon">
          <i className="icon icon-energy" />
          {i18n.t('mist.nodeInfo.syncStarting')}
        </div>
      </div>
    );
  }

  localStatsSyncProgress() {
    const { highestBlock, currentBlock, startingBlock } = this.props.local.sync;

    let displayBlock =
      this.props.local.sync.displayBlock || this.props.local.sync.startingBlock;
    displayBlock += (currentBlock - displayBlock) / 20;
    let formattedDisplayBlock = numeral(displayBlock).format('0,0');

    this.props.local.sync.displayBlock = displayBlock;

    const progress =
      ((displayBlock - startingBlock) / (highestBlock - startingBlock)) * 100;

    return (
      <div>
        <div className="block-number row-icon">
          <i className="icon icon-layers" />
          {formattedDisplayBlock}
        </div>
        <div className="peer-count row-icon">
          <i className="icon icon-users" />
          {` ${this.props.local.peerCount} ${i18n.t('mist.nodeInfo.peers')}`}
        </div>
        <div className="sync-progress row-icon">
          <i className="icon icon-cloud-download" />
          <progress max="100" value={progress || 0} />
        </div>
      </div>
    );
  }

  renderTimeSince(diff) {
    return (
      <div
        title={i18n.t('mist.nodeInfo.timeSinceBlock')}
        className={
          diff > 60 ? 'block-diff row-icon red' : 'block-diff row-icon'
        }
      >
        {
          // TODO: make this i8n compatible
        }
        <i className="icon icon-clock" />
        {diff < 120 ? diff + ' seconds' : Math.floor(diff / 60) + ' minutes'}
      </div>
    );
  }

  localStatsSynced() {
    const { blockNumber, timestamp, syncMode } = this.props.local;
    const formattedBlockNumber = numeral(blockNumber).format('0,0');

    const timeSince = moment(timestamp, 'X');
    const diff = moment().diff(timeSince, 'seconds');

    return (
      <div>
        <div
          className="block-number row-icon"
          title={i18n.t('mist.nodeInfo.blockNumber')}
        >
          <i className="icon icon-layers" /> {formattedBlockNumber}
        </div>
        {this.props.network !== 'private' && (
          <div className="peer-count row-icon">
            <i className="icon icon-users" />
            {` ${this.props.local.peerCount} ${i18n.t('mist.nodeInfo.peers')}`}
          </div>
        )}
        {this.renderTimeSince(diff)}
      </div>
    );
  }

  renderLocalStats() {
    const { syncMode } = this.props.local;
    const { currentBlock } = this.props.local.sync;

    let syncText;
    if (syncMode) {
      syncText = syncMode === 'nosync' ? `sync off` : `${syncMode} sync`;
    }

    let localStats;

    // TODO: potentially refactor local node status into Redux;
    // possible states: findingPeers, starting, synced, synced, disabled/nosync

    // Determine 'status' of local node, then show appropriate lens on sync data
    if (syncMode === 'nosync') {
      // Case: no local node
      return null;
    } else if (this.props.active === 'local') {
      // Case: already synced up
      localStats = this.localStatsSynced();
    } else if (this.props.active === 'remote') {
      // Case: not yet synced up
      if (currentBlock === 0) {
        // Case: no results from syncing
        if (this.props.local.peerCount === 0) {
          // Case: no peers yet
          localStats = this.localStatsFindingPeers();
        } else {
          // Case: connected to peers, but no blocks yet
          localStats = this.localStatsStartSync();
        }
      } else {
        // Case: show progress
        localStats = this.localStatsSyncProgress();
      }
    }

    return (
      <div id="local-stats" className="node-info__section">
        <div className="node-info__node-title local">
          <strong>{i18n.t('mist.nodeInfo.local')}</strong>{' '}
          {i18n.t('mist.nodeInfo.node')}
          {syncText && <span className="node-info__pill">{syncText}</span>}
        </div>

        {localStats}
      </div>
    );
  }

  render() {
    const { active, network, remote, local } = this.props;

    let mainClass = network == 'main' ? 'node-mainnet' : 'node-testnet';
    if (this.state.sticky) mainClass += ' sticky';

    return (
      <div
        id="node-info"
        className={mainClass}
        onMouseUp={() => this.setState({ sticky: !this.state.sticky })}
        onMouseEnter={() => this.setState({ showSubmenu: true })}
        onMouseLeave={() => this.setState({ showSubmenu: this.state.sticky })}
      >
        <StatusLight
          active={active}
          network={network}
          remote={remote}
          local={local}
        />

        {this.state.showSubmenu && (
          <section className="node-info__submenu-container">
            <section>
              <div className="node-info__section">
                <div className="node-info__network-title">{network}</div>
                <div className="node-info__subtitle">
                  {network !== 'main' && i18n.t('mist.nodeInfo.testNetwork')}
                  {network === 'main' && i18n.t('mist.nodeInfo.network')}
                </div>
              </div>

              {this.renderRemoteStats()}

              {this.renderLocalStats()}
            </section>
          </section>
        )}
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    active: state.nodes.active,
    network: state.nodes.network,
    remote: state.nodes.remote,
    local: state.nodes.local
  };
}

export default connect(mapStateToProps)(NodeInfo);