github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/clusterOverview/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 d3 from "d3"; 13 import React from "react"; 14 import { Helmet } from "react-helmet"; 15 import { connect } from "react-redux"; 16 import { createSelector } from "reselect"; 17 18 import { AdminUIState } from "src/redux/state"; 19 import { nodesSummarySelector, NodesSummary } from "src/redux/nodes"; 20 import { Bytes as formatBytes } from "src/util/format"; 21 import createChartComponent from "src/views/shared/util/d3-react"; 22 import capacityChart from "./capacity"; 23 import spinner from "assets/spinner.gif"; 24 import { refreshNodes, refreshLiveness } from "src/redux/apiReducers"; 25 import EmailSubscription from "src/views/dashboard/emailSubscription"; 26 import "./cluster.styl"; 27 28 // tslint:disable-next-line:variable-name 29 const CapacityChart = createChartComponent("svg", capacityChart()); 30 31 interface CapacityUsageProps { 32 usedCapacity: number; 33 usableCapacity: number; 34 } 35 36 const formatPercentage = d3.format("0.1%"); 37 38 function renderCapacityUsage(props: CapacityUsageProps) { 39 const { usedCapacity, usableCapacity } = props; 40 const usedPercentage = usableCapacity !== 0 ? usedCapacity / usableCapacity : 0; 41 return [ 42 <h3 className="capacity-usage cluster-summary__title">Capacity Usage</h3>, 43 <div className="capacity-usage cluster-summary__label storage-percent">Used<br />Percent</div>, 44 <div className="capacity-usage cluster-summary__metric storage-percent">{ formatPercentage(usedPercentage) }</div>, 45 <div className="capacity-usage cluster-summary__chart"> 46 <CapacityChart used={usedCapacity} usable={usableCapacity} /> 47 </div>, 48 <div className="capacity-usage cluster-summary__label storage-used">Used<br />Capacity</div>, 49 <div className="capacity-usage cluster-summary__metric storage-used">{ formatBytes(usedCapacity) }</div>, 50 <div className="capacity-usage cluster-summary__label storage-usable">Usable<br />Capacity</div>, 51 <div className="capacity-usage cluster-summary__metric storage-usable">{ formatBytes(usableCapacity) }</div>, 52 ]; 53 } 54 55 const mapStateToCapacityUsageProps = createSelector( 56 nodesSummarySelector, 57 function (nodesSummary: NodesSummary) { 58 const { capacityUsed, capacityUsable } = nodesSummary.nodeSums; 59 return { 60 usedCapacity: capacityUsed, 61 usableCapacity: capacityUsable, 62 }; 63 }, 64 ); 65 66 interface NodeLivenessProps { 67 liveNodes: number; 68 suspectNodes: number; 69 deadNodes: number; 70 } 71 72 function renderNodeLiveness(props: NodeLivenessProps) { 73 const { liveNodes, suspectNodes, deadNodes } = props; 74 const suspectClasses = classNames( 75 "node-liveness", 76 "cluster-summary__metric", 77 "suspect-nodes", 78 { 79 "warning": suspectNodes > 0, 80 "disabled": suspectNodes === 0, 81 }, 82 ); 83 const deadClasses = classNames( 84 "node-liveness", 85 "cluster-summary__metric", 86 "dead-nodes", 87 { 88 "alert": deadNodes > 0, 89 "disabled": deadNodes === 0, 90 }, 91 ); 92 return [ 93 <h3 className="node-liveness cluster-summary__title">Node Status</h3>, 94 <div className="node-liveness cluster-summary__metric live-nodes">{ liveNodes }</div>, 95 <div className="node-liveness cluster-summary__label live-nodes">Live<br />Nodes</div>, 96 <div className={suspectClasses}>{ suspectNodes }</div>, 97 <div className="node-liveness cluster-summary__label suspect-nodes">Suspect<br />Nodes</div>, 98 <div className={deadClasses}>{ deadNodes }</div>, 99 <div className="node-liveness cluster-summary__label dead-nodes">Dead<br />Nodes</div>, 100 ]; 101 } 102 103 const mapStateToNodeLivenessProps = createSelector( 104 nodesSummarySelector, 105 function (nodesSummary: NodesSummary) { 106 const { nodeCounts } = nodesSummary.nodeSums; 107 return { 108 liveNodes: nodeCounts.healthy, 109 suspectNodes: nodeCounts.suspect, 110 deadNodes: nodeCounts.dead, 111 }; 112 }, 113 ); 114 115 interface ReplicationStatusProps { 116 totalRanges: number; 117 underReplicatedRanges: number; 118 unavailableRanges: number; 119 } 120 121 function renderReplicationStatus(props: ReplicationStatusProps) { 122 const { totalRanges, underReplicatedRanges, unavailableRanges } = props; 123 const underReplicatedClasses = classNames( 124 "replication-status", 125 "cluster-summary__metric", 126 "under-replicated-ranges", 127 { 128 "warning": underReplicatedRanges > 0, 129 "disabled": underReplicatedRanges === 0, 130 }, 131 ); 132 const unavailableClasses = classNames( 133 "replication-status", 134 "cluster-summary__metric", 135 "unavailable-ranges", 136 { 137 "alert": unavailableRanges > 0, 138 "disabled": unavailableRanges === 0, 139 }, 140 ); 141 return [ 142 <h3 className="replication-status cluster-summary__title">Replication Status</h3>, 143 <div className="replication-status cluster-summary__metric total-ranges">{ totalRanges }</div>, 144 <div className="replication-status cluster-summary__label total-ranges">Total<br />Ranges</div>, 145 <div className={underReplicatedClasses}>{ underReplicatedRanges }</div>, 146 <div className="replication-status cluster-summary__label under-replicated-ranges">Under-replicated<br />Ranges</div>, 147 <div className={unavailableClasses}>{ unavailableRanges }</div>, 148 <div className="replication-status cluster-summary__label unavailable-ranges">Unavailable<br />Ranges</div>, 149 ]; 150 } 151 152 const mapStateToReplicationStatusProps = createSelector( 153 nodesSummarySelector, 154 function (nodesSummary: NodesSummary) { 155 const { totalRanges, underReplicatedRanges, unavailableRanges } = nodesSummary.nodeSums; 156 return { 157 totalRanges: totalRanges, 158 underReplicatedRanges: underReplicatedRanges, 159 unavailableRanges: unavailableRanges, 160 }; 161 }, 162 ); 163 164 interface ClusterSummaryStateProps { 165 capacityUsage: CapacityUsageProps; 166 nodeLiveness: NodeLivenessProps; 167 replicationStatus: ReplicationStatusProps; 168 loading: boolean; 169 } 170 interface ClusterSummaryActionsProps { 171 refreshLiveness: () => void; 172 refreshNodes: () => void; 173 } 174 175 type ClusterSummaryProps = ClusterSummaryStateProps & ClusterSummaryActionsProps; 176 177 class ClusterSummary extends React.Component<ClusterSummaryProps, {}> { 178 componentDidMount() { 179 this.refresh(); 180 } 181 182 componentDidUpdate() { 183 this.refresh(); 184 } 185 186 refresh() { 187 this.props.refreshLiveness(); 188 this.props.refreshNodes(); 189 } 190 191 render() { 192 const children = []; 193 194 if (this.props.loading) { 195 children.push(<img className="visualization__spinner" src={spinner} />); 196 } else { 197 children.push( 198 ...renderCapacityUsage(this.props.capacityUsage), 199 ...renderNodeLiveness(this.props.nodeLiveness), 200 ...renderReplicationStatus(this.props.replicationStatus), 201 ); 202 } 203 204 return <section className="cluster-summary" children={React.Children.toArray(children)} />; 205 } 206 } 207 208 function mapStateToClusterSummaryProps(state: AdminUIState) { 209 return { 210 capacityUsage: mapStateToCapacityUsageProps(state), 211 nodeLiveness: mapStateToNodeLivenessProps(state), 212 replicationStatus: mapStateToReplicationStatusProps(state), 213 loading: !state.cachedData.nodes.data, 214 }; 215 } 216 217 const actions = { 218 refreshLiveness: refreshLiveness, 219 refreshNodes: refreshNodes, 220 }; 221 222 // tslint:disable-next-line:variable-name 223 const ClusterSummaryConnected = connect(mapStateToClusterSummaryProps, actions)(ClusterSummary); 224 225 /** 226 * Renders the main content of the cluster visualization page. 227 */ 228 export default class ClusterOverview extends React.Component<any, any> { 229 render() { 230 return ( 231 <div className="cluster-page"> 232 <Helmet title="Cluster Overview" /> 233 <EmailSubscription /> 234 <section className="section cluster-overview"> 235 <ClusterSummaryConnected /> 236 </section> 237 <section className="cluster-overview--fixed"> 238 { this.props.children } 239 </section> 240 </div> 241 ); 242 } 243 }