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 }