github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/reports/containers/range/rangeTable.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 classNames from "classnames";
    12  import _ from "lodash";
    13  import Long from "long";
    14  import moment from "moment";
    15  import React from "react";
    16  import * as protos from "src/js/protos";
    17  import { cockroach } from "src/js/protos";
    18  import { LongToMoment, NanoToMilli, SecondsToNano } from "src/util/convert";
    19  import { FixLong } from "src/util/fixLong";
    20  import { Bytes } from "src/util/format";
    21  import Lease from "src/views/reports/containers/range/lease";
    22  import Print from "src/views/reports/containers/range/print";
    23  import RangeInfo from "src/views/reports/containers/range/rangeInfo";
    24  
    25  interface RangeTableProps {
    26    infos: protos.cockroach.server.serverpb.IRangeInfo[];
    27    replicas: protos.cockroach.roachpb.IReplicaDescriptor[];
    28  }
    29  
    30  interface RangeTableRow {
    31    readonly variable: string;
    32    readonly display: string;
    33    readonly compareToLeader: boolean; // When true, displays a warning when a
    34    // value doesn't match the leader's.
    35  }
    36  
    37  interface RangeTableCellContent {
    38    value: string[];
    39    title?: string[];
    40    className?: string[];
    41  }
    42  
    43  interface RangeTableDetail {
    44    [name: string]: RangeTableCellContent;
    45  }
    46  
    47  const rangeTableDisplayList: RangeTableRow[] = [
    48    { variable: "id", display: "ID", compareToLeader: false },
    49    { variable: "keyRange", display: "Key Range", compareToLeader: true },
    50    { variable: "problems", display: "Problems", compareToLeader: true },
    51    { variable: "raftState", display: "Raft State", compareToLeader: false },
    52    { variable: "quiescent", display: "Quiescent", compareToLeader: true },
    53    { variable: "ticking", display: "Ticking", compareToLeader: true },
    54    { variable: "leaseType", display: "Lease Type", compareToLeader: true },
    55    { variable: "leaseState", display: "Lease State", compareToLeader: true },
    56    { variable: "leaseHolder", display: "Lease Holder", compareToLeader: true },
    57    { variable: "leaseEpoch", display: "Lease Epoch", compareToLeader: true },
    58    { variable: "leaseStart", display: "Lease Start", compareToLeader: true },
    59    { variable: "leaseExpiration", display: "Lease Expiration", compareToLeader: true },
    60    { variable: "leaseAppliedIndex", display: "Lease Applied Index", compareToLeader: true },
    61    { variable: "raftLeader", display: "Raft Leader", compareToLeader: true },
    62    { variable: "vote", display: "Vote", compareToLeader: false },
    63    { variable: "term", display: "Term", compareToLeader: true },
    64    { variable: "leadTransferee", display: "Lead Transferee", compareToLeader: false },
    65    { variable: "applied", display: "Applied", compareToLeader: true },
    66    { variable: "commit", display: "Commit", compareToLeader: true },
    67    { variable: "lastIndex", display: "Last Index", compareToLeader: true },
    68    { variable: "logSize", display: "Log Size", compareToLeader: false },
    69    { variable: "leaseHolderQPS", display: "Lease Holder QPS", compareToLeader: false },
    70    { variable: "keysWrittenPS", display: "Average Keys Written Per Second", compareToLeader: false },
    71    { variable: "approxProposalQuota", display: "Approx Proposal Quota", compareToLeader: false },
    72    { variable: "pendingCommands", display: "Pending Commands", compareToLeader: false },
    73    { variable: "droppedCommands", display: "Dropped Commands", compareToLeader: false },
    74    { variable: "truncatedIndex", display: "Truncated Index", compareToLeader: true },
    75    { variable: "truncatedTerm", display: "Truncated Term", compareToLeader: true },
    76    { variable: "mvccLastUpdate", display: "MVCC Last Update", compareToLeader: true },
    77    { variable: "GCAvgAge", display: "Dead Value average age", compareToLeader: true},
    78    { variable: "GCBytesAge", display: "GC Bytes Age (score)", compareToLeader: true},
    79    { variable: "NumIntents", display: "Intents", compareToLeader: true},
    80    { variable: "IntentAvgAge", display: "Intent Average Age", compareToLeader: true},
    81    { variable: "IntentAge", display: "Intent Age (score)", compareToLeader: true },
    82    { variable: "mvccLiveBytesCount", display: "MVCC Live Bytes/Count", compareToLeader: true },
    83    { variable: "mvccKeyBytesCount", display: "MVCC Key Bytes/Count", compareToLeader: true },
    84    { variable: "mvccValueBytesCount", display: "MVCC Value Bytes/Count", compareToLeader: true },
    85    { variable: "mvccIntentBytesCount", display: "MVCC Intent Bytes/Count", compareToLeader: true },
    86    { variable: "mvccSystemBytesCount", display: "MVCC System Bytes/Count", compareToLeader: true },
    87    { variable: "rangeMaxBytes", display: "Max Range Size Before Split", compareToLeader: true },
    88    { variable: "writeLatches", display: "Write Latches Local/Global", compareToLeader: false },
    89    { variable: "readLatches", display: "Read Latches Local/Global", compareToLeader: false },
    90  ];
    91  
    92  const rangeTableEmptyContent: RangeTableCellContent = {
    93    value: ["-"],
    94  };
    95  
    96  const rangeTableEmptyContentWithWarning: RangeTableCellContent = {
    97    value: ["-"],
    98    className: ["range-table__cell--warning"],
    99  };
   100  
   101  const rangeTableQuiescent: RangeTableCellContent = {
   102    value: ["quiescent"],
   103    className: ["range-table__cell--quiescent"],
   104  };
   105  
   106  function convertLeaseState(leaseState: protos.cockroach.kv.kvserver.storagepb.LeaseState) {
   107    return protos.cockroach.kv.kvserver.storagepb.LeaseState[leaseState].toLowerCase();
   108  }
   109  
   110  export default class RangeTable extends React.Component<RangeTableProps, {}> {
   111    cleanRaftState(state: string) {
   112      switch (_.toLower(state)) {
   113        case "statedormant": return "dormant";
   114        case "stateleader": return "leader";
   115        case "statefollower": return "follower";
   116        case "statecandidate": return "candidate";
   117        case "stateprecandidate": return "precandidate";
   118        default: return "unknown";
   119      }
   120    }
   121  
   122    contentRaftState(state: string): RangeTableCellContent {
   123      const cleanedState = this.cleanRaftState(state);
   124      return {
   125        value: [cleanedState],
   126        className: [`range-table__cell--raftstate-${cleanedState}`],
   127      };
   128    }
   129  
   130    contentNanos(nanos: Long): RangeTableCellContent {
   131      const humanized = Print.Time(LongToMoment(nanos));
   132      return {
   133        value: [humanized],
   134        title: [humanized, nanos.toString()],
   135      };
   136    }
   137  
   138    contentDuration(nanos: Long): RangeTableCellContent {
   139      const humanized = Print.Duration(moment.duration(NanoToMilli(nanos.toNumber())));
   140      return {
   141        value: [humanized],
   142        title: [humanized, nanos.toString()],
   143      };
   144    }
   145  
   146    contentMVCC(bytes: Long, count: Long): RangeTableCellContent {
   147      const humanizedBytes = Bytes(bytes.toNumber());
   148      return {
   149        value: [`${humanizedBytes} / ${count.toString()} count`],
   150        title: [`${humanizedBytes} / ${count.toString()} count`,
   151        `${bytes.toString()} bytes / ${count.toString()} count`],
   152      };
   153    }
   154  
   155    contentBytes(bytes: Long, className: string = null, toolTip: string = null): RangeTableCellContent {
   156      const humanized = Bytes(bytes.toNumber());
   157      if (_.isNull(className)) {
   158        return {
   159          value: [humanized],
   160          title: [humanized, bytes.toString()],
   161        };
   162      }
   163      return {
   164        value: [humanized],
   165        title: [humanized, bytes.toString(), toolTip],
   166        className: [className],
   167      };
   168    }
   169  
   170    contentGCAvgAge(mvcc: cockroach.storage.enginepb.IMVCCStats): RangeTableCellContent {
   171      if (mvcc === null) {
   172        return this.contentDuration(Long.fromNumber(0));
   173      }
   174      const deadBytes = mvcc.key_bytes.add(mvcc.val_bytes).sub(mvcc.live_bytes);
   175      if (!deadBytes.eq(0)) {
   176        const avgDeadByteAgeSec = mvcc.gc_bytes_age.div(deadBytes);
   177        return this.contentDuration(Long.fromNumber(SecondsToNano(avgDeadByteAgeSec.toNumber())));
   178      } else {
   179        return this.contentDuration(Long.fromNumber(0));
   180      }
   181    }
   182  
   183    createContentIntentAvgAge(mvcc: cockroach.storage.enginepb.IMVCCStats): RangeTableCellContent {
   184      if (mvcc === null) {
   185        return this.contentDuration(Long.fromNumber(0));
   186      }
   187      if (!mvcc.intent_count.eq(0)) {
   188        const avgIntentAgeSec = mvcc.intent_age.div(mvcc.intent_count);
   189        return this.contentDuration(Long.fromNumber(SecondsToNano(avgIntentAgeSec.toNumber())));
   190      } else {
   191        return this.contentDuration(Long.fromNumber(0));
   192      }
   193    }
   194  
   195    createContent(value: string | Long | number, className: string = null): RangeTableCellContent {
   196      if (_.isNull(className)) {
   197        return {
   198          value: [value.toString()],
   199        };
   200      }
   201      return {
   202        value: [value.toString()],
   203        className: [className],
   204      };
   205    }
   206  
   207    contentLatchInfo(
   208      local: Long | number, global: Long | number, isRaftLeader: boolean,
   209    ): RangeTableCellContent {
   210      if (isRaftLeader) {
   211        return this.createContent(`${local.toString()} local / ${global.toString()} global`);
   212      }
   213      if (local.toString() === "0" && global.toString() === "0") {
   214        return rangeTableEmptyContent;
   215      }
   216      return this.createContent(
   217        `${local.toString()} local / ${global.toString()} global`,
   218        "range-table__cell--warning",
   219      );
   220    }
   221  
   222    contentTimestamp(timestamp: protos.cockroach.util.hlc.ITimestamp): RangeTableCellContent {
   223      if (_.isNil(timestamp) || _.isNil(timestamp.wall_time)) {
   224        return {
   225          value: ["no timestamp"],
   226          className: ["range-table__cell--warning"],
   227        };
   228      }
   229      const humanized = Print.Timestamp(timestamp);
   230      return {
   231        value: [humanized],
   232        title: [humanized, FixLong(timestamp.wall_time).toString()],
   233      };
   234    }
   235  
   236    contentProblems(
   237      problems: protos.cockroach.server.serverpb.IRangeProblems,
   238      awaitingGC: boolean,
   239    ): RangeTableCellContent {
   240      let results: string[] = [];
   241      if (problems.no_lease) {
   242        results = _.concat(results, "Invalid Lease");
   243      }
   244      if (problems.leader_not_lease_holder) {
   245        results = _.concat(results, "Leader is Not Lease holder");
   246      }
   247      if (problems.underreplicated) {
   248        results = _.concat(results, "Underreplicated (or slow)");
   249      }
   250      if (problems.overreplicated) {
   251        results = _.concat(results, "Overreplicated");
   252      }
   253      if (problems.no_raft_leader) {
   254        results = _.concat(results, "No Raft Leader");
   255      }
   256      if (problems.unavailable) {
   257        results = _.concat(results, "Unavailable");
   258      }
   259      if (problems.quiescent_equals_ticking) {
   260        results = _.concat(results, "Quiescent equals ticking");
   261      }
   262      if (problems.raft_log_too_large) {
   263        results = _.concat(results, "Raft log too large");
   264      }
   265      if (awaitingGC) {
   266        results = _.concat(results, "Awaiting GC");
   267      }
   268      return {
   269        value: results,
   270        title: results,
   271        className: results.length > 0 ? ["range-table__cell--warning"] : [],
   272      };
   273    }
   274  
   275    // contentIf returns an empty value if the condition is false, and if true,
   276    // executes and returns the content function.
   277    contentIf(
   278      showContent: boolean,
   279      content: () => RangeTableCellContent,
   280    ): RangeTableCellContent {
   281      if (!showContent) {
   282        return rangeTableEmptyContent;
   283      }
   284      return content();
   285    }
   286  
   287    renderRangeCell(
   288      row: RangeTableRow,
   289      cell: RangeTableCellContent,
   290      key: number,
   291      dormant: boolean,
   292      leaderCell?: RangeTableCellContent,
   293    ) {
   294      const title = _.join(_.isNil(cell.title) ? cell.value : cell.title, "\n");
   295      const differentFromLeader = !dormant && !_.isNil(leaderCell) && row.compareToLeader && (!_.isEqual(cell.value, leaderCell.value) || !_.isEqual(cell.title, leaderCell.title));
   296      const className = classNames(
   297        "range-table__cell",
   298        {
   299          "range-table__cell--dormant": dormant,
   300          "range-table__cell--different-from-leader-warning": differentFromLeader,
   301        },
   302        (!dormant && !_.isNil(cell.className) ? cell.className : []),
   303      );
   304      return (
   305        <td key={key} className={className} title={title}>
   306          <ul className="range-entries-list">
   307            {
   308              _.map(cell.value, (value, k) => (
   309                <li key={k}>
   310                  {value}
   311                </li>
   312              ))
   313            }
   314          </ul>
   315        </td>
   316      );
   317    }
   318  
   319    renderRangeRow(
   320      row: RangeTableRow,
   321      detailsByStoreID: Map<number, RangeTableDetail>,
   322      dormantStoreIDs: Set<number>,
   323      leaderStoreID: number,
   324      sortedStoreIDs: number[],
   325      key: number,
   326    ) {
   327      const leaderDetail = detailsByStoreID.get(leaderStoreID);
   328      const values: Set<string> = new Set();
   329      if (row.compareToLeader) {
   330        detailsByStoreID.forEach((detail, storeID) => {
   331          if (!dormantStoreIDs.has(storeID)) {
   332            values.add(_.join(detail[row.variable].value, " "));
   333          }
   334        });
   335      }
   336      const headerClassName = classNames(
   337        "range-table__cell",
   338        "range-table__cell--header",
   339        { "range-table__cell--header-warning": values.size > 1 },
   340      );
   341      return (
   342        <tr key={key} className="range-table__row">
   343          <th className={headerClassName}>
   344            {row.display}
   345          </th>
   346          {
   347            _.map(sortedStoreIDs, (storeID) => {
   348              const cell = detailsByStoreID.get(storeID)[row.variable];
   349              const leaderCell = (storeID === leaderStoreID) ? null : leaderDetail[row.variable];
   350              return (
   351                this.renderRangeCell(
   352                  row,
   353                  cell,
   354                  storeID,
   355                  dormantStoreIDs.has(storeID),
   356                  leaderCell,
   357                )
   358              );
   359            })
   360          }
   361        </tr>
   362      );
   363    }
   364  
   365    renderRangeReplicaCell(
   366      leaderReplicaIDs: Set<number>,
   367      replicaID: number,
   368      replica: protos.cockroach.roachpb.IReplicaDescriptor,
   369      rangeID: Long,
   370      localStoreID: number,
   371      dormant: boolean,
   372    ) {
   373      const differentFromLeader = !dormant && (_.isNil(replica) ? leaderReplicaIDs.has(replicaID) : !leaderReplicaIDs.has(replica.replica_id));
   374      const localReplica = !dormant && !differentFromLeader && replica && replica.store_id === localStoreID;
   375      const className = classNames({
   376        "range-table__cell": true,
   377        "range-table__cell--dormant": dormant,
   378        "range-table__cell--different-from-leader-warning": differentFromLeader,
   379        "range-table__cell--local-replica": localReplica,
   380      });
   381      if (_.isNil(replica)) {
   382        return (
   383          <td key={localStoreID} className={className}>
   384            -
   385          </td>
   386        );
   387      }
   388      const value = Print.ReplicaID(rangeID, replica);
   389      return (
   390        <td key={localStoreID} className={className} title={value}>
   391          {value}
   392        </td>
   393      );
   394    }
   395  
   396    renderRangeReplicaRow(
   397      replicasByReplicaIDByStoreID: Map<number, Map<number, protos.cockroach.roachpb.IReplicaDescriptor>>,
   398      referenceReplica: protos.cockroach.roachpb.IReplicaDescriptor,
   399      leaderReplicaIDs: Set<number>,
   400      dormantStoreIDs: Set<number>,
   401      sortedStoreIDs: number[],
   402      rangeID: Long,
   403      key: string,
   404    ) {
   405      const headerClassName = "range-table__cell range-table__cell--header";
   406      return (
   407        <tr key={key} className="range-table__row">
   408          <th className={headerClassName}>
   409            Replica {referenceReplica.replica_id} - ({Print.ReplicaID(rangeID, referenceReplica)})
   410          </th>
   411          {
   412            _.map(sortedStoreIDs, storeID => {
   413              let replica: protos.cockroach.roachpb.IReplicaDescriptor = null;
   414              if (replicasByReplicaIDByStoreID.has(storeID) &&
   415                replicasByReplicaIDByStoreID.get(storeID).has(referenceReplica.replica_id)) {
   416                replica = replicasByReplicaIDByStoreID.get(storeID).get(referenceReplica.replica_id);
   417              }
   418              return this.renderRangeReplicaCell(
   419                leaderReplicaIDs,
   420                referenceReplica.replica_id,
   421                replica,
   422                rangeID,
   423                storeID,
   424                dormantStoreIDs.has(storeID),
   425              );
   426            })
   427          }
   428        </tr>
   429      );
   430    }
   431  
   432    render() {
   433      const { infos, replicas } = this.props;
   434      const leader = _.head(infos);
   435      const rangeID = leader.state.state.desc.range_id;
   436      const data = _.chain(infos);
   437  
   438      // We want to display ordered by store ID.
   439      const sortedStoreIDs = data
   440        .map(info => info.source_store_id)
   441        .sortBy(id => id)
   442        .value();
   443  
   444      const dormantStoreIDs: Set<number> = new Set();
   445  
   446      // Convert the infos to a simpler object for display purposes. This helps when trying to
   447      // determine if any warnings should be displayed.
   448      const detailsByStoreID: Map<number, RangeTableDetail> = new Map();
   449      _.forEach(infos, info => {
   450        const localReplica = RangeInfo.GetLocalReplica(info);
   451        const awaitingGC = _.isNil(localReplica);
   452        const lease = info.state.state.lease;
   453        const epoch = Lease.IsEpoch(lease);
   454        const raftLeader = !awaitingGC && FixLong(info.raft_state.lead).eq(localReplica.replica_id);
   455        const leaseHolder = !awaitingGC && localReplica.replica_id === lease.replica.replica_id;
   456        const mvcc = info.state.state.stats;
   457        const raftState = this.contentRaftState(info.raft_state.state);
   458        const vote = FixLong(info.raft_state.hard_state.vote);
   459        let leaseState: RangeTableCellContent;
   460        if (_.isNil(info.lease_status)) {
   461          leaseState = rangeTableEmptyContentWithWarning;
   462        } else {
   463          leaseState = this.createContent(
   464            convertLeaseState(info.lease_status.state),
   465            info.lease_status.state === protos.cockroach.kv.kvserver.storagepb.LeaseState.VALID ? "" :
   466              "range-table__cell--warning",
   467          );
   468        }
   469        const dormant = raftState.value[0] === "dormant";
   470        if (dormant) {
   471          dormantStoreIDs.add(info.source_store_id);
   472        }
   473        detailsByStoreID.set(info.source_store_id, {
   474          id: this.createContent(Print.ReplicaID(
   475            rangeID,
   476            localReplica,
   477            info.source_node_id,
   478            info.source_store_id,
   479          )),
   480          keyRange: this.createContent(`${info.span.start_key} to ${info.span.end_key}`),
   481          problems: this.contentProblems(info.problems, awaitingGC),
   482          raftState: raftState,
   483          quiescent: info.quiescent ? rangeTableQuiescent : rangeTableEmptyContent,
   484          ticking: this.createContent(info.ticking.toString()),
   485          leaseState: leaseState,
   486          leaseHolder: this.createContent(
   487            Print.ReplicaID(rangeID, lease.replica),
   488            leaseHolder ? "range-table__cell--lease-holder" : "range-table__cell--lease-follower",
   489          ),
   490          leaseType: this.createContent(epoch ? "epoch" : "expiration"),
   491          leaseEpoch: epoch ? this.createContent(lease.epoch) : rangeTableEmptyContent,
   492          leaseStart: this.contentTimestamp(lease.start),
   493          leaseExpiration: epoch ? rangeTableEmptyContent : this.contentTimestamp(lease.expiration),
   494          leaseAppliedIndex: this.createContent(FixLong(info.state.state.lease_applied_index)),
   495          raftLeader: this.contentIf(!dormant, () => this.createContent(
   496            FixLong(info.raft_state.lead),
   497            raftLeader ? "range-table__cell--raftstate-leader" : "range-table__cell--raftstate-follower",
   498          )),
   499          vote: this.contentIf(!dormant, () => this.createContent(vote.greaterThan(0) ? vote : "-")),
   500          term: this.contentIf(!dormant, () => this.createContent(FixLong(info.raft_state.hard_state.term))),
   501          leadTransferee: this.contentIf(!dormant, () => {
   502            const leadTransferee = FixLong(info.raft_state.lead_transferee);
   503            return this.createContent(leadTransferee.greaterThan(0) ? leadTransferee : "-");
   504          }),
   505          applied: this.contentIf(!dormant, () => this.createContent(FixLong(info.raft_state.applied))),
   506          commit: this.contentIf(!dormant, () => this.createContent(FixLong(info.raft_state.hard_state.commit))),
   507          lastIndex: this.createContent(FixLong(info.state.last_index)),
   508          logSize: this.contentBytes(
   509            FixLong(info.state.raft_log_size),
   510            info.state.raft_log_size_trusted ? "" : "range-table__cell--dormant",
   511            "Log size is known to not be correct. This isn't an error condition. " +
   512              "The log size will became exact the next time it is recomputed. " +
   513              "This replica does not perform log truncation (because the log might already " +
   514              "be truncated sufficiently).",
   515          ),
   516          leaseHolderQPS: leaseHolder ? this.createContent(info.stats.queries_per_second.toFixed(4)) : rangeTableEmptyContent,
   517          keysWrittenPS: this.createContent(info.stats.writes_per_second.toFixed(4)),
   518          approxProposalQuota: raftLeader ? this.createContent(FixLong(info.state.approximate_proposal_quota)) : rangeTableEmptyContent,
   519          pendingCommands: this.createContent(FixLong(info.state.num_pending)),
   520          droppedCommands: this.createContent(
   521            FixLong(info.state.num_dropped),
   522            FixLong(info.state.num_dropped).greaterThan(0) ? "range-table__cell--warning" : "",
   523          ),
   524          truncatedIndex: this.createContent(FixLong(info.state.state.truncated_state.index)),
   525          truncatedTerm: this.createContent(FixLong(info.state.state.truncated_state.term)),
   526          mvccLastUpdate: this.contentNanos(FixLong(mvcc.last_update_nanos)),
   527          mvccLiveBytesCount: this.contentMVCC(FixLong(mvcc.live_bytes), FixLong(mvcc.live_count)),
   528          mvccKeyBytesCount: this.contentMVCC(FixLong(mvcc.key_bytes), FixLong(mvcc.key_count)),
   529          mvccValueBytesCount: this.contentMVCC(FixLong(mvcc.val_bytes), FixLong(mvcc.val_count)),
   530          mvccIntentBytesCount: this.contentMVCC(FixLong(mvcc.intent_bytes), FixLong(mvcc.intent_count)),
   531          mvccSystemBytesCount: this.contentMVCC(FixLong(mvcc.sys_bytes), FixLong(mvcc.sys_count)),
   532          rangeMaxBytes: this.contentBytes(FixLong(info.state.range_max_bytes)),
   533          mvccIntentAge: this.contentDuration(FixLong(mvcc.intent_age)),
   534  
   535          GCAvgAge: this.contentGCAvgAge(mvcc),
   536          GCBytesAge: this.createContent(FixLong(mvcc.gc_bytes_age)),
   537  
   538          NumIntents: this.createContent(FixLong(mvcc.intent_count)),
   539          IntentAvgAge: this.createContentIntentAvgAge(mvcc),
   540          IntentAge: this.createContent(FixLong(mvcc.intent_age)),
   541  
   542          writeLatches: this.contentLatchInfo(
   543            FixLong(info.latches_local.write_count),
   544            FixLong(info.latches_global.write_count),
   545            raftLeader,
   546          ),
   547          readLatches: this.contentLatchInfo(
   548            FixLong(info.latches_local.read_count),
   549            FixLong(info.latches_global.read_count),
   550            raftLeader,
   551          ),
   552        });
   553      });
   554  
   555      const leaderReplicaIDs = new Set(_.map(leader.state.state.desc.internal_replicas, rep => rep.replica_id));
   556  
   557      // Go through all the replicas and add them to map for easy printing.
   558      const replicasByReplicaIDByStoreID: Map<number, Map<number, protos.cockroach.roachpb.IReplicaDescriptor>> = new Map();
   559      _.forEach(infos, info => {
   560        const replicasByReplicaID: Map<number, protos.cockroach.roachpb.IReplicaDescriptor> = new Map();
   561        _.forEach(info.state.state.desc.internal_replicas, rep => {
   562          replicasByReplicaID.set(rep.replica_id, rep);
   563        });
   564        replicasByReplicaIDByStoreID.set(info.source_store_id, replicasByReplicaID);
   565      });
   566  
   567      return (
   568        <div>
   569          <h2 className="base-heading">Range r{rangeID.toString()} at {Print.Time(moment().utc())} UTC</h2>
   570          <table className="range-table">
   571            <tbody>
   572              {
   573                _.map(rangeTableDisplayList, (title, key) => (
   574                  this.renderRangeRow(
   575                    title,
   576                    detailsByStoreID,
   577                    dormantStoreIDs,
   578                    leader.source_store_id,
   579                    sortedStoreIDs,
   580                    key,
   581                  )
   582                ))
   583              }
   584              {
   585                _.map(replicas, (replica, key) => (
   586                  this.renderRangeReplicaRow(
   587                    replicasByReplicaIDByStoreID,
   588                    replica,
   589                    leaderReplicaIDs,
   590                    dormantStoreIDs,
   591                    sortedStoreIDs,
   592                    rangeID,
   593                    "replica" + key,
   594                  )
   595                ))
   596              }
   597            </tbody>
   598          </table>
   599        </div>
   600      );
   601    }
   602  }