github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/devtools/containers/raftRanges/index.tsx (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import _ from "lodash";
    12  import React from "react";
    13  import ReactPaginate from "react-paginate";
    14  import { connect } from "react-redux";
    15  import { Link, withRouter } from "react-router-dom";
    16  import * as protos from "src/js/protos";
    17  import { refreshRaft } from "src/redux/apiReducers";
    18  import { CachedDataReducerState } from "src/redux/cachedDataReducer";
    19  import { AdminUIState } from "src/redux/state";
    20  import { ToolTipWrapper } from "src/views/shared/components/toolTip";
    21  
    22  /******************************
    23   *   RAFT RANGES MAIN COMPONENT
    24   */
    25  
    26  const RANGES_PER_PAGE = 100;
    27  
    28  /**
    29   * RangesMainData are the data properties which should be passed to the RangesMain
    30   * container.
    31   */
    32  interface RangesMainData {
    33    state: CachedDataReducerState<protos.cockroach.server.serverpb.RaftDebugResponse>;
    34  }
    35  
    36  /**
    37   * RangesMainActions are the action dispatchers which should be passed to the
    38   * RangesMain container.
    39   */
    40  interface RangesMainActions {
    41    // Call if the ranges statuses are stale and need to be refreshed.
    42    refreshRaft: typeof refreshRaft;
    43  }
    44  
    45  interface RangesMainState {
    46    showState?: boolean;
    47    showReplicas?: boolean;
    48    showPending?: boolean;
    49    showOnlyErrors?: boolean;
    50    pageNum?: number;
    51    offset?: number;
    52  }
    53  
    54  /**
    55   * RangesMainProps is the type of the props object that must be passed to
    56   * RangesMain component.
    57   */
    58  type RangesMainProps = RangesMainData & RangesMainActions;
    59  
    60  /**
    61   * Renders the main content of the raft ranges page, which is primarily a data
    62   * table of all ranges and their replicas.
    63   */
    64  export class RangesMain extends React.Component<RangesMainProps, RangesMainState> {
    65    state: RangesMainState = {
    66      showState: true,
    67      showReplicas: true,
    68      showPending: true,
    69      showOnlyErrors: false,
    70      offset: 0,
    71    };
    72  
    73    componentDidMount() {
    74      // Refresh nodes status query when mounting.
    75      this.props.refreshRaft();
    76    }
    77  
    78    componentDidUpdate() {
    79      // Refresh ranges when props are received; this will immediately
    80      // trigger a new request if previous results are invalidated.
    81      if (!this.props.state.valid) {
    82        this.props.refreshRaft();
    83      }
    84    }
    85  
    86    renderPagination(pageCount: number): React.ReactNode {
    87      return <ReactPaginate previousLabel={"previous"}
    88        nextLabel={"next"}
    89        breakLabel={"..."}
    90        pageCount={pageCount}
    91        marginPagesDisplayed={2}
    92        pageRangeDisplayed={5}
    93        onPageChange={this.handlePageClick.bind(this)}
    94        containerClassName={"pagination"}
    95        activeClassName={"active"} />;
    96    }
    97  
    98    handlePageClick(data: any) {
    99      const selected = data.selected;
   100      const offset = Math.ceil(selected * RANGES_PER_PAGE);
   101      this.setState({ offset });
   102      window.scroll(0, 0);
   103    }
   104  
   105    // renderFilterSettings renders the filter settings box.
   106    renderFilterSettings(): React.ReactNode {
   107      return <div className="section raft-filters">
   108        <b>Filters</b>
   109        <label>
   110          <input type="checkbox" checked={this.state.showState}
   111            onChange={() => this.setState({ showState: !this.state.showState })} />
   112          State
   113        </label>
   114        <label>
   115          <input type="checkbox" checked={this.state.showReplicas}
   116            onChange={() => this.setState({ showReplicas: !this.state.showReplicas })} />
   117          Replicas
   118        </label>
   119        <label>
   120          <input type="checkbox" checked={this.state.showPending}
   121            onChange={() => this.setState({ showPending: !this.state.showPending })} />
   122          Pending
   123        </label>
   124        <label>
   125          <input type="checkbox" checked={this.state.showOnlyErrors}
   126            onChange={() => this.setState({ showOnlyErrors: !this.state.showOnlyErrors })} />
   127          Only Error Ranges
   128        </label>
   129      </div>;
   130    }
   131  
   132    render() {
   133      const statuses = this.props.state.data;
   134      let content: React.ReactNode = null;
   135      let errors: string[] = [];
   136  
   137      if (this.props.state.lastError) {
   138        errors.push(this.props.state.lastError.message);
   139      }
   140  
   141      if (!this.props.state.data) {
   142        content = <div className="section">Loading...</div>;
   143      } else if (statuses) {
   144        errors = errors.concat(statuses.errors.map(err => err.message));
   145  
   146        // Build list of all nodes for static ordering.
   147        const nodeIDs = _(statuses.ranges).flatMap((range: protos.cockroach.server.serverpb.IRaftRangeStatus) => {
   148          return range.nodes;
   149        }).map((node: protos.cockroach.server.serverpb.IRaftRangeNode) => {
   150          return node.node_id;
   151        }).uniq().sort().value();
   152  
   153        const nodeIDIndex: { [nodeID: number]: number } = {};
   154        const columns = [<th key={-1}>Range</th>];
   155        nodeIDs.forEach((id, i) => {
   156          nodeIDIndex[id] = i + 1;
   157          columns.push((
   158            <th key={i}>
   159              <Link className="debug-link" to={"/nodes/" + id}>Node {id}</Link>
   160            </th>
   161          ));
   162        });
   163  
   164        // Filter ranges and paginate
   165        const justRanges = _.values(statuses.ranges);
   166        const filteredRanges = _.filter(justRanges, (range) => {
   167          return !this.state.showOnlyErrors || range.errors.length > 0;
   168        });
   169        let offset = this.state.offset;
   170        if (this.state.offset > filteredRanges.length) {
   171          offset = 0;
   172        }
   173        const ranges = filteredRanges.slice(offset, offset + RANGES_PER_PAGE);
   174        const rows: React.ReactNode[][] = [];
   175        _.map(ranges, (range, i) => {
   176          const hasErrors = range.errors.length > 0;
   177          const rangeErrors = <ul>{_.map(range.errors, (error, j) => {
   178            return <li key={j}>{error.message}</li>;
   179          })}</ul>;
   180          const row = [<td key="row{i}">
   181            <Link className="debug-link" to={`/reports/range/${range.range_id.toString()}`}>
   182              r{range.range_id.toString()}
   183            </Link>
   184            {
   185              (hasErrors) ? (
   186                <span style={{ position: "relative" }}>
   187                  <ToolTipWrapper text={rangeErrors}>
   188                    <div className="viz-info-icon">
   189                      <div className="icon-warning" />
   190                    </div>
   191                  </ToolTipWrapper>
   192                </span>
   193              ) : ""
   194            }
   195          </td>];
   196          rows[i] = row;
   197  
   198          // Render each replica into a cell
   199          range.nodes.forEach((node) => {
   200            const nodeRange = node.range;
   201            const replicaLocations = nodeRange.state.state.desc.internal_replicas.map(
   202              (replica) => "(Node " + replica.node_id.toString() +
   203                " Store " + replica.store_id.toString() +
   204                " ReplicaID " + replica.replica_id.toString() + ")",
   205            );
   206            const display = (l?: Long): string => {
   207              if (l) {
   208                return l.toString();
   209              }
   210              return "N/A";
   211            };
   212            const index = nodeIDIndex[node.node_id];
   213            const raftState = nodeRange.raft_state;
   214            const cell = <td key={index}>
   215              {(this.state.showState) ? <div>
   216                State: {raftState.state}&nbsp;
   217                  ReplicaID={display(raftState.replica_id)}&nbsp;
   218                  Term={display(raftState.hard_state.term)}&nbsp;
   219                  Lead={display(raftState.lead)}&nbsp;
   220              </div> : ""}
   221              {(this.state.showReplicas) ? <div>
   222                <div>Replica On: {replicaLocations.join(", ")}</div>
   223                <div>Next Replica ID: {nodeRange.state.state.desc.next_replica_id}</div>
   224              </div> : ""}
   225              {(this.state.showPending) ? <div>Pending Command Count: {(nodeRange.state.num_pending || 0).toString()}</div> : ""}
   226            </td>;
   227            row[index] = cell;
   228          });
   229  
   230          // Fill empty spaces in table with td elements.
   231          for (let j = 1; j <= nodeIDs.length; j++) {
   232            if (!row[j]) {
   233              row[j] = <td key={j}></td>;
   234            }
   235          }
   236        });
   237  
   238        // Build the final display table
   239        if (columns.length > 1) {
   240          content = <div>
   241            {this.renderFilterSettings()}
   242            <table>
   243              <thead><tr>{columns}</tr></thead>
   244              <tbody>
   245                {_.values(rows).map((row: React.ReactNode[], i: number) => {
   246                  return <tr key={i}>{row}</tr>;
   247                })}
   248              </tbody>
   249            </table>
   250            <div className="section">
   251              {this.renderPagination(Math.ceil(filteredRanges.length / RANGES_PER_PAGE))}
   252            </div>
   253          </div>;
   254        }
   255      }
   256      return <div className="section table">
   257        {this.props.children}
   258        <div className="stats-table">
   259          {this.renderErrors(errors)}
   260          {content}
   261        </div>
   262      </div>;
   263    }
   264  
   265    renderErrors(errors: string[]) {
   266      if (!errors || errors.length === 0) {
   267        return;
   268      }
   269      return <div className="section">
   270        {errors.map((err: string, i: number) => {
   271          return <div key={i}>Error: {err}</div>;
   272        })}
   273      </div>;
   274    }
   275  }
   276  
   277  /******************************
   278   *         SELECTORS
   279   */
   280  
   281  // Base selectors to extract data from redux state.
   282  const selectRaftState = (state: AdminUIState): CachedDataReducerState<protos.cockroach.server.serverpb.RaftDebugResponse> => state.cachedData.raft;
   283  
   284  const mapStateToProps = (state: AdminUIState) => ({ // RootState contains declaration for whole state
   285    state: selectRaftState(state),
   286  });
   287  
   288  const mapDispatchToProps = {
   289    refreshRaft,
   290  };
   291  
   292  // Connect the RangesMain class with our redux store.
   293  const rangesMainConnected = withRouter(connect(
   294    mapStateToProps,
   295    mapDispatchToProps,
   296  )(RangesMain));
   297  
   298  export { rangesMainConnected as default };