github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/nodeOverview/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 { Helmet } from "react-helmet"; 14 import { connect } from "react-redux"; 15 import { Link, RouteComponentProps, withRouter } from "react-router-dom"; 16 import { createSelector } from "reselect"; 17 import { refreshLiveness, refreshNodes } from "src/redux/apiReducers"; 18 import { livenessNomenclature, LivenessStatus, NodesSummary, nodesSummarySelector, selectNodesSummaryValid } from "src/redux/nodes"; 19 import { AdminUIState } from "src/redux/state"; 20 import { nodeIDAttr } from "src/util/constants"; 21 import { LongToMoment } from "src/util/convert"; 22 import { Bytes, DATE_FORMAT, Percentage } from "src/util/format"; 23 import { INodeStatus, MetricConstants, StatusMetrics } from "src/util/proto"; 24 import { getMatchParamByName } from "src/util/query"; 25 import { SummaryBar, SummaryLabel, SummaryValue } from "src/views/shared/components/summaryBar"; 26 import { Button, BackIcon } from "src/components/button"; 27 import "./nodeOverview.styl"; 28 29 /** 30 * TableRow is a small stateless component that renders a single row in the node 31 * overview table. Each row renders a store metrics value, comparing the value 32 * across the different stores on the node (along with a total value for the 33 * node itself). 34 */ 35 function TableRow(props: { data: INodeStatus, title: string, valueFn: (s: StatusMetrics) => React.ReactNode }) { 36 return <tr className="table__row table__row--body"> 37 <td className="table__cell">{ props.title }</td> 38 <td className="table__cell">{ props.valueFn(props.data.metrics) }</td> 39 { 40 _.map(props.data.store_statuses, (ss) => { 41 return <td key={ss.desc.store_id} className="table__cell">{ props.valueFn(ss.metrics) }</td>; 42 }) 43 } 44 <td className="table__cell table__cell--filler" /> 45 </tr>; 46 } 47 48 interface NodeOverviewProps extends RouteComponentProps { 49 node: INodeStatus; 50 nodesSummary: NodesSummary; 51 refreshNodes: typeof refreshNodes; 52 refreshLiveness: typeof refreshLiveness; 53 // True if current status results are still valid. Needed so that this 54 // component refreshes status query when it becomes invalid. 55 nodesSummaryValid: boolean; 56 } 57 58 /** 59 * Renders the Node Overview page. 60 */ 61 export class NodeOverview extends React.Component<NodeOverviewProps, {}> { 62 componentDidMount() { 63 // Refresh nodes status query when mounting. 64 this.props.refreshNodes(); 65 this.props.refreshLiveness(); 66 } 67 68 componentDidUpdate() { 69 // Refresh nodes status query when props are received; this will immediately 70 // trigger a new request if previous results are invalidated. 71 this.props.refreshNodes(); 72 this.props.refreshLiveness(); 73 } 74 75 prevPage = () => this.props.history.goBack(); 76 77 render() { 78 const { node, nodesSummary } = this.props; 79 if (!node) { 80 return ( 81 <div className="section"> 82 <h1 className="base-heading">Loading cluster status...</h1> 83 </div> 84 ); 85 } 86 87 const liveness = nodesSummary.livenessStatusByNodeID[node.desc.node_id] || LivenessStatus.LIVE; 88 const livenessString = livenessNomenclature(liveness); 89 90 return ( 91 <div> 92 <Helmet title={`${nodesSummary.nodeDisplayNameByID[node.desc.node_id]} | Nodes`} /> 93 <div className="section section--heading"> 94 <Button 95 onClick={this.prevPage} 96 type="flat" 97 size="small" 98 className="crl-button--link-to" 99 icon={BackIcon} 100 iconPosition="left" 101 > 102 Overview 103 </Button> 104 <h2 className="base-heading">{`Node ${node.desc.node_id} / ${node.desc.address.address_field}`}</h2> 105 </div> 106 <section className="section l-columns"> 107 <div className="l-columns__left"> 108 <table className="table"> 109 <thead> 110 <tr className="table__row table__row--header"> 111 <th className="table__cell" /> 112 <th className="table__cell">{`Node ${node.desc.node_id}`}</th> 113 { 114 _.map(node.store_statuses, (ss) => { 115 const storeId = ss.desc.store_id; 116 return <th key={storeId} className="table__cell">{`Store ${storeId}`}</th>; 117 }) 118 } 119 <th className="table__cell table__cell--filler" /> 120 </tr> 121 </thead> 122 <tbody> 123 <TableRow data={node} 124 title="Live Bytes" 125 valueFn={(metrics) => Bytes(metrics[MetricConstants.liveBytes])} /> 126 <TableRow data={node} 127 title="Key Bytes" 128 valueFn={(metrics) => Bytes(metrics[MetricConstants.keyBytes])} /> 129 <TableRow data={node} 130 title="Value Bytes" 131 valueFn={(metrics) => Bytes(metrics[MetricConstants.valBytes])} /> 132 <TableRow data={node} 133 title="Intent Bytes" 134 valueFn={(metrics) => Bytes(metrics[MetricConstants.intentBytes])} /> 135 <TableRow data={node} 136 title="Sys Bytes" 137 valueFn={(metrics) => Bytes(metrics[MetricConstants.sysBytes])} /> 138 <TableRow data={node} 139 title="GC Bytes Age" 140 valueFn={(metrics) => metrics[MetricConstants.gcBytesAge].toString()} /> 141 <TableRow data={node} 142 title="Total Replicas" 143 valueFn={(metrics) => metrics[MetricConstants.replicas].toString()} /> 144 <TableRow data={node} 145 title="Raft Leaders" 146 valueFn={(metrics) => metrics[MetricConstants.raftLeaders].toString()} /> 147 <TableRow data={node} 148 title="Total Ranges" 149 valueFn={(metrics) => metrics[MetricConstants.ranges]} /> 150 <TableRow data={node} 151 title="Unavailable %" 152 valueFn={(metrics) => Percentage(metrics[MetricConstants.unavailableRanges], metrics[MetricConstants.ranges])} /> 153 <TableRow data={node} 154 title="Under Replicated %" 155 valueFn={(metrics) => Percentage(metrics[MetricConstants.underReplicatedRanges], metrics[MetricConstants.ranges])} /> 156 <TableRow data={node} 157 title="Used Capacity" 158 valueFn={(metrics) => Bytes(metrics[MetricConstants.usedCapacity])} /> 159 <TableRow data={node} 160 title="Available Capacity" 161 valueFn={(metrics) => Bytes(metrics[MetricConstants.availableCapacity])} /> 162 <TableRow data={node} 163 title="Total Capacity" 164 valueFn={(metrics) => Bytes(metrics[MetricConstants.capacity])} /> 165 </tbody> 166 </table> 167 </div> 168 <div className="l-columns__right"> 169 <SummaryBar> 170 <SummaryLabel>Node Summary</SummaryLabel> 171 <SummaryValue 172 title="Health" 173 value={livenessString} 174 classModifier={livenessString} 175 /> 176 <SummaryValue title="Last Update" value={LongToMoment(node.updated_at).format(DATE_FORMAT)} /> 177 <SummaryValue title="Build" value={node.build_info.tag} /> 178 <SummaryValue 179 title="Logs" 180 value={<Link to={`/node/${node.desc.node_id}/logs`}>View Logs</Link>} 181 classModifier="link" 182 /> 183 </SummaryBar> 184 </div> 185 </section> 186 </div> 187 ); 188 } 189 } 190 191 export const currentNode = createSelector( 192 (state: AdminUIState, _props: RouteComponentProps): INodeStatus[] => state.cachedData.nodes.data, 193 (_state: AdminUIState, props: RouteComponentProps): number => parseInt(getMatchParamByName(props.match, nodeIDAttr), 10), 194 (nodes, id) => { 195 if (!nodes || !id) { 196 return undefined; 197 } 198 return _.find(nodes, (ns) => ns.desc.node_id === id); 199 }); 200 201 export default withRouter(connect( 202 (state: AdminUIState, ownProps: RouteComponentProps) => { 203 return { 204 node: currentNode(state, ownProps), 205 nodesSummary: nodesSummarySelector(state), 206 nodesSummaryValid: selectNodesSummaryValid(state), 207 }; 208 }, 209 { 210 refreshNodes, 211 refreshLiveness, 212 }, 213 )(NodeOverview));