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} 217 ReplicaID={display(raftState.replica_id)} 218 Term={display(raftState.hard_state.term)} 219 Lead={display(raftState.lead)} 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 };