github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/reports/containers/nodes/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 classNames from "classnames"; 12 import _ from "lodash"; 13 import Long from "long"; 14 import moment from "moment"; 15 import React from "react"; 16 import { Helmet } from "react-helmet"; 17 import { connect } from "react-redux"; 18 import { withRouter, RouteComponentProps } from "react-router-dom"; 19 20 import * as protos from "src/js/protos"; 21 import { refreshLiveness, refreshNodes } from "src/redux/apiReducers"; 22 import { nodesSummarySelector, NodesSummary } from "src/redux/nodes"; 23 import { AdminUIState } from "src/redux/state"; 24 import { LongToMoment } from "src/util/convert"; 25 import { FixLong } from "src/util/fixLong"; 26 import { getFilters, localityToString, NodeFilterList } from "src/views/reports/components/nodeFilterList"; 27 28 interface NodesOwnProps { 29 nodesSummary: NodesSummary; 30 refreshNodes: typeof refreshNodes; 31 refreshLiveness: typeof refreshLiveness; 32 } 33 34 interface NodesTableRowParams { 35 title: string; 36 extract: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => React.ReactNode; 37 equality?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string; 38 cellTitle?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string; 39 } 40 41 type NodesProps = NodesOwnProps & RouteComponentProps; 42 43 const dateFormat = "Y-MM-DD HH:mm:ss"; 44 const detailTimeFormat = "Y/MM/DD HH:mm:ss"; 45 46 const loading = ( 47 <div className="section"> 48 <h1 className="base-heading">Node Diagnostics</h1> 49 <h2 className="base-heading">Loading cluster status...</h2> 50 </div> 51 ); 52 53 function NodeTableCell(props: { value: React.ReactNode, title: string }) { 54 return ( 55 <td className="nodes-table__cell" title={props.title}> 56 {props.value} 57 </td> 58 ); 59 } 60 61 // Functions starting with "print" return a single string representation which 62 // can be used for title, the main content or even equality comparisons. 63 function printNodeID(status: protos.cockroach.server.status.statuspb.INodeStatus) { 64 return `n${status.desc.node_id}`; 65 } 66 67 function printSingleValue(value: string) { 68 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 69 return _.get(status, value, null); 70 }; 71 } 72 73 function printSingleValueWithFunction(value: string, fn: (item: any) => string) { 74 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 75 return fn(_.get(status, value, null)); 76 }; 77 } 78 79 function printMultiValue(value: string) { 80 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 81 return _.join(_.get(status, value, []), "\n"); 82 }; 83 } 84 85 function printDateValue(value: string, inputDateFormat: string) { 86 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 87 if (!_.has(status, value)) { 88 return null; 89 } 90 return moment(_.get(status, value), inputDateFormat).format(dateFormat); 91 }; 92 } 93 94 function printTimestampValue(value: string) { 95 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 96 if (!_.has(status, value)) { 97 return null; 98 } 99 return LongToMoment(FixLong(_.get(status, value) as Long)).format(dateFormat); 100 }; 101 } 102 103 // Functions starting with "title" are used exclusively to print the cell 104 // titles. They always return a single string. 105 function titleDateValue(value: string, inputDateFormat: string) { 106 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 107 if (!_.has(status, value)) { 108 return null; 109 } 110 const raw = _.get(status, value); 111 return `${moment(raw, inputDateFormat).format(dateFormat)}\n${raw}`; 112 }; 113 } 114 115 function titleTimestampValue(value: string) { 116 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 117 if (!_.has(status, value)) { 118 return null; 119 } 120 const raw = FixLong(_.get(status, value) as Long); 121 return `${LongToMoment(raw).format(dateFormat)}\n${raw.toString()}`; 122 }; 123 } 124 125 // Functions starting with "extract" are used exclusively for for extracting 126 // the main content of a cell. 127 function extractMultiValue(value: string) { 128 return function (status: protos.cockroach.server.status.statuspb.INodeStatus) { 129 const items = _.map(_.get(status, value, []), item => item.toString()); 130 return ( 131 <ul className="nodes-entries-list"> 132 { 133 _.map(items, (item, key) => ( 134 <li key={key} className="nodes-entries-list--item"> 135 {item} 136 </li> 137 )) 138 } 139 </ul> 140 ); 141 }; 142 } 143 144 function extractCertificateLink(status: protos.cockroach.server.status.statuspb.INodeStatus) { 145 const nodeID = status.desc.node_id; 146 return ( 147 <a className="debug-link" href={`#/reports/certificates/${nodeID}`}> 148 n{nodeID} Certificates 149 </a> 150 ); 151 } 152 153 const nodesTableRows: NodesTableRowParams[] = [ 154 { 155 title: "Node ID", 156 extract: printNodeID, 157 }, 158 { 159 title: "Address", 160 extract: printSingleValue("desc.address.address_field"), 161 cellTitle: printSingleValue("desc.address.address_field"), 162 }, 163 { 164 title: "Locality", 165 extract: printSingleValueWithFunction("desc.locality", localityToString), 166 cellTitle: printSingleValueWithFunction("desc.locality", localityToString), 167 }, 168 { 169 title: "Certificates", 170 extract: extractCertificateLink, 171 }, 172 { 173 title: "Attributes", 174 extract: extractMultiValue("desc.attrs.attrs"), 175 cellTitle: printMultiValue("desc.attrs.attrs"), 176 }, 177 { 178 title: "Environment", 179 extract: extractMultiValue("env"), 180 cellTitle: printMultiValue("env"), 181 }, 182 { 183 title: "Arguments", 184 extract: extractMultiValue("args"), 185 cellTitle: printMultiValue("args"), 186 }, 187 { 188 title: "Tag", 189 extract: printSingleValue("build_info.tag"), 190 cellTitle: printSingleValue("build_info.tag"), 191 equality: printSingleValue("build_info.tag"), 192 }, 193 { 194 title: "Revision", 195 extract: printSingleValue("build_info.revision"), 196 cellTitle: printSingleValue("build_info.revision"), 197 equality: printSingleValue("build_info.revision"), 198 }, 199 { 200 title: "Time", 201 extract: printDateValue("build_info.time", detailTimeFormat), 202 cellTitle: titleDateValue("build_info.time", detailTimeFormat), 203 equality: printDateValue("build_info.time", detailTimeFormat), 204 }, 205 { 206 title: "Type", 207 extract: printSingleValue("build_info.type"), 208 cellTitle: printSingleValue("build_info.type"), 209 equality: printSingleValue("build_info.type"), 210 }, 211 { 212 title: "Platform", 213 extract: printSingleValue("build_info.platform"), 214 cellTitle: printSingleValue("build_info.platform"), 215 equality: printSingleValue("build_info.platform"), 216 }, 217 { 218 title: "Go Version", 219 extract: printSingleValue("build_info.go_version"), 220 cellTitle: printSingleValue("build_info.go_version"), 221 equality: printSingleValue("build_info.go_version"), 222 }, 223 { 224 title: "CGO", 225 extract: printSingleValue("build_info.cgo_compiler"), 226 cellTitle: printSingleValue("build_info.cgo_compiler"), 227 equality: printSingleValue("build_info.cgo_compiler"), 228 }, 229 { 230 title: "Distribution", 231 extract: printSingleValue("build_info.distribution"), 232 cellTitle: printSingleValue("build_info.distribution"), 233 equality: printSingleValue("build_info.distribution"), 234 }, 235 { 236 title: "Started at", 237 extract: printTimestampValue("started_at"), 238 cellTitle: titleTimestampValue("started_at"), 239 }, 240 { 241 title: "Updated at", 242 extract: printTimestampValue("updated_at"), 243 cellTitle: titleTimestampValue("updated_at"), 244 }, 245 ]; 246 247 /** 248 * Renders the Nodes Diagnostics Report page. 249 */ 250 export class Nodes extends React.Component<NodesProps, {}> { 251 refresh(props = this.props) { 252 props.refreshLiveness(); 253 props.refreshNodes(); 254 } 255 256 componentDidMount() { 257 // Refresh nodes status query when mounting. 258 this.refresh(); 259 } 260 261 componentDidUpdate(prevProps: NodesProps) { 262 if (!_.isEqual(this.props.location, prevProps.location)) { 263 this.refresh(this.props); 264 } 265 } 266 267 renderNodesTableRow( 268 orderedNodeIDs: string[], 269 key: number, 270 title: string, 271 extract: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => React.ReactNode, 272 equality?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string, 273 cellTitle?: (ns: protos.cockroach.server.status.statuspb.INodeStatus) => string, 274 ) { 275 const inconsistent = !_.isNil(equality) && _.chain(orderedNodeIDs) 276 .map(nodeID => this.props.nodesSummary.nodeStatusByID[nodeID]) 277 .map(status => equality(status)) 278 .uniq() 279 .value() 280 .length > 1; 281 const headerClassName = classNames( 282 "nodes-table__cell", 283 "nodes-table__cell--header", 284 { "nodes-table__cell--header-warning": inconsistent }, 285 ); 286 287 return ( 288 <tr className="nodes-table__row" key={key}> 289 <th className={headerClassName}> 290 {title} 291 </th> 292 { 293 _.map(orderedNodeIDs, nodeID => { 294 const status = this.props.nodesSummary.nodeStatusByID[nodeID]; 295 return ( 296 <NodeTableCell 297 key={nodeID} 298 value={extract(status)} 299 title={_.isNil(cellTitle) ? null : cellTitle(status)} 300 /> 301 ); 302 }) 303 } 304 </tr> 305 ); 306 } 307 308 render() { 309 const { nodesSummary } = this.props; 310 const { nodeStatusByID } = nodesSummary; 311 if (_.isEmpty(nodesSummary.nodeIDs)) { 312 return loading; 313 } 314 315 const filters = getFilters(this.props.location); 316 317 let nodeIDsContext = _.chain(nodesSummary.nodeIDs) 318 .map((nodeID: string) => Number.parseInt(nodeID, 10)); 319 if (!_.isNil(filters.nodeIDs) && filters.nodeIDs.size > 0) { 320 nodeIDsContext = nodeIDsContext.filter(nodeID => filters.nodeIDs.has(nodeID)); 321 } 322 if (!_.isNil(filters.localityRegex)) { 323 nodeIDsContext = nodeIDsContext.filter(nodeID => ( 324 filters.localityRegex.test(localityToString(nodeStatusByID[nodeID.toString()].desc.locality)) 325 )); 326 } 327 328 // Sort the node IDs and then convert them back to string for lookups. 329 const orderedNodeIDs = nodeIDsContext 330 .orderBy(nodeID => nodeID) 331 .map(nodeID => nodeID.toString()) 332 .value(); 333 334 if (_.isEmpty(orderedNodeIDs)) { 335 return ( 336 <section className="section"> 337 <h1 className="base-heading">Node Diagnostics</h1> 338 <NodeFilterList nodeIDs={filters.nodeIDs} localityRegex={filters.localityRegex} /> 339 <h2 className="base-heading">No nodes match the filters</h2> 340 </section> 341 ); 342 } 343 344 return ( 345 <section className="section"> 346 <Helmet title="Node Diagnostics | Debug" /> 347 <h1 className="base-heading">Node Diagnostics</h1> 348 <NodeFilterList nodeIDs={filters.nodeIDs} localityRegex={filters.localityRegex} /> 349 <h2 className="base-heading">Nodes</h2> 350 <table className="nodes-table"> 351 <tbody> 352 { 353 _.map(nodesTableRows, (row, key) => { 354 return this.renderNodesTableRow( 355 orderedNodeIDs, 356 key, 357 row.title, 358 row.extract, 359 row.equality, 360 row.cellTitle, 361 ); 362 }) 363 } 364 </tbody> 365 </table> 366 </section> 367 ); 368 } 369 } 370 371 const mapStateToProps = (state: AdminUIState) => ({ 372 nodesSummary: nodesSummarySelector(state), 373 }); 374 375 const mapDispatchToProps = { 376 refreshNodes, 377 refreshLiveness, 378 }; 379 380 export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nodes));