github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/nodeGraphs/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 { createSelector } from "reselect"; 16 import { withRouter, RouteComponentProps } from "react-router-dom"; 17 18 import { 19 nodeIDAttr, dashboardNameAttr, 20 } from "src/util/constants"; 21 import Dropdown, { DropdownOption } from "src/views/shared/components/dropdown"; 22 import { PageConfig, PageConfigItem } from "src/views/shared/components/pageconfig"; 23 import TimeScaleDropdown from "src/views/cluster/containers/timescale"; 24 import ClusterSummaryBar from "./summaryBar"; 25 26 import { AdminUIState } from "src/redux/state"; 27 import { refreshNodes, refreshLiveness } from "src/redux/apiReducers"; 28 import { hoverStateSelector, HoverState, hoverOn, hoverOff } from "src/redux/hover"; 29 import { nodesSummarySelector, NodesSummary, LivenessStatus } from "src/redux/nodes"; 30 import Alerts from "src/views/shared/containers/alerts"; 31 import { MetricsDataProvider } from "src/views/shared/containers/metricDataProvider"; 32 33 import { 34 GraphDashboardProps, storeIDsForNode, 35 } from "./dashboards/dashboardUtils"; 36 37 import overviewDashboard from "./dashboards/overview"; 38 import runtimeDashboard from "./dashboards/runtime"; 39 import sqlDashboard from "./dashboards/sql"; 40 import storageDashboard from "./dashboards/storage"; 41 import replicationDashboard from "./dashboards/replication"; 42 import distributedDashboard from "./dashboards/distributed"; 43 import queuesDashboard from "./dashboards/queues"; 44 import requestsDashboard from "./dashboards/requests"; 45 import hardwareDashboard from "./dashboards/hardware"; 46 import changefeedsDashboard from "./dashboards/changefeeds"; 47 import { getMatchParamByName } from "src/util/query"; 48 49 interface GraphDashboard { 50 label: string; 51 component: (props: GraphDashboardProps) => React.ReactElement<any>[]; 52 } 53 54 const dashboards: {[key: string]: GraphDashboard} = { 55 "overview" : { label: "Overview", component: overviewDashboard }, 56 "hardware": { label: "Hardware", component: hardwareDashboard }, 57 "runtime" : { label: "Runtime", component: runtimeDashboard }, 58 "sql": { label: "SQL", component: sqlDashboard }, 59 "storage": { label: "Storage", component: storageDashboard }, 60 "replication": { label: "Replication", component: replicationDashboard }, 61 "distributed": { label: "Distributed", component: distributedDashboard }, 62 "queues": { label: "Queues", component: queuesDashboard }, 63 "requests": { label: "Slow Requests", component: requestsDashboard }, 64 "changefeeds": { label: "Changefeeds", component: changefeedsDashboard }, 65 }; 66 67 const defaultDashboard = "overview"; 68 69 const dashboardDropdownOptions = _.map(dashboards, (dashboard, key) => { 70 return { 71 value: key, 72 label: dashboard.label, 73 }; 74 }); 75 76 // The properties required by a NodeGraphs component. 77 interface NodeGraphsOwnProps { 78 refreshNodes: typeof refreshNodes; 79 refreshLiveness: typeof refreshLiveness; 80 hoverOn: typeof hoverOn; 81 hoverOff: typeof hoverOff; 82 nodesQueryValid: boolean; 83 livenessQueryValid: boolean; 84 nodesSummary: NodesSummary; 85 hoverState: HoverState; 86 } 87 88 type NodeGraphsProps = NodeGraphsOwnProps & RouteComponentProps; 89 90 /** 91 * NodeGraphs renders the main content of the cluster graphs page. 92 */ 93 export class NodeGraphs extends React.Component<NodeGraphsProps, {}> { 94 /** 95 * Selector to compute node dropdown options from the current node summary 96 * collection. 97 */ 98 private nodeDropdownOptions = createSelector( 99 (summary: NodesSummary) => summary.nodeStatuses, 100 (summary: NodesSummary) => summary.nodeDisplayNameByID, 101 (summary: NodesSummary) => summary.livenessStatusByNodeID, 102 (nodeStatuses, nodeDisplayNameByID, livenessStatusByNodeID): DropdownOption[] => { 103 const base = [{value: "", label: "Cluster"}]; 104 return base.concat( 105 _.chain(nodeStatuses) 106 .filter(ns => livenessStatusByNodeID[ns.desc.node_id] !== LivenessStatus.DECOMMISSIONED) 107 .map(ns => ({ 108 value: ns.desc.node_id.toString(), 109 label: nodeDisplayNameByID[ns.desc.node_id], 110 })) 111 .value(), 112 ); 113 }, 114 ); 115 116 refresh(props = this.props) { 117 if (!props.nodesQueryValid) { 118 props.refreshNodes(); 119 } 120 if (!props.livenessQueryValid) { 121 props.refreshLiveness(); 122 } 123 } 124 125 setClusterPath(nodeID: string, dashboardName: string) { 126 const push = this.props.history.push; 127 if (!_.isString(nodeID) || nodeID === "") { 128 push(`/metrics/${dashboardName}/cluster`); 129 } else { 130 push(`/metrics/${dashboardName}/node/${nodeID}`); 131 } 132 } 133 134 nodeChange = (selected: DropdownOption) => { 135 this.setClusterPath(selected.value, getMatchParamByName(this.props.match, dashboardNameAttr)); 136 } 137 138 dashChange = (selected: DropdownOption) => { 139 this.setClusterPath(getMatchParamByName(this.props.match, nodeIDAttr), selected.value); 140 } 141 142 componentDidMount() { 143 this.refresh(); 144 } 145 146 componentDidUpdate() { 147 this.refresh(this.props); 148 } 149 150 render() { 151 const { match, nodesSummary } = this.props; 152 const selectedDashboard = getMatchParamByName(match, dashboardNameAttr); 153 const dashboard = _.has(dashboards, selectedDashboard) 154 ? selectedDashboard 155 : defaultDashboard; 156 157 const title = dashboards[dashboard].label + " Dashboard"; 158 const selectedNode = getMatchParamByName(match, nodeIDAttr) || ""; 159 const nodeSources = (selectedNode !== "") ? [selectedNode] : null; 160 161 // When "all" is the selected source, some graphs display a line for every 162 // node in the cluster using the nodeIDs collection. However, if a specific 163 // node is already selected, these per-node graphs should only display data 164 // only for the selected node. 165 const nodeIDs = nodeSources ? nodeSources : nodesSummary.nodeIDs; 166 167 // If a single node is selected, we need to restrict the set of stores 168 // queried for per-store metrics (only stores that belong to that node will 169 // be queried). 170 const storeSources = nodeSources ? storeIDsForNode(nodesSummary, nodeSources[0]) : null; 171 172 // tooltipSelection is a string used in tooltips to reference the currently 173 // selected nodes. This is a prepositional phrase, currently either "across 174 // all nodes" or "on node X". 175 const tooltipSelection = (nodeSources && nodeSources.length === 1) 176 ? `on node ${nodeSources[0]}` 177 : "across all nodes"; 178 179 const dashboardProps: GraphDashboardProps = { 180 nodeIDs, 181 nodesSummary, 182 nodeSources, 183 storeSources, 184 tooltipSelection, 185 }; 186 187 const forwardParams = { 188 hoverOn: this.props.hoverOn, 189 hoverOff: this.props.hoverOff, 190 hoverState: this.props.hoverState, 191 }; 192 193 // Generate graphs for the current dashboard, wrapping each one in a 194 // MetricsDataProvider with a unique key. 195 const graphs = dashboards[dashboard].component(dashboardProps); 196 const graphComponents = _.map(graphs, (graph, idx) => { 197 const key = `nodes.${dashboard}.${idx}`; 198 return ( 199 <div key={key}> 200 <MetricsDataProvider id={key}> 201 { React.cloneElement(graph, forwardParams) } 202 </MetricsDataProvider> 203 </div> 204 ); 205 }); 206 207 return ( 208 <div> 209 <Helmet title={title} /> 210 <section className="section"><h1 className="base-heading">{ title }</h1></section> 211 <PageConfig> 212 <PageConfigItem> 213 <Dropdown 214 title="Graph" 215 options={this.nodeDropdownOptions(this.props.nodesSummary)} 216 selected={selectedNode} 217 onChange={this.nodeChange} 218 /> 219 </PageConfigItem> 220 <PageConfigItem> 221 <Dropdown 222 title="Dashboard" 223 options={dashboardDropdownOptions} 224 selected={dashboard} 225 onChange={this.dashChange} 226 className="full-size" 227 /> 228 </PageConfigItem> 229 <PageConfigItem> 230 <TimeScaleDropdown /> 231 </PageConfigItem> 232 </PageConfig> 233 <section className="section"> 234 <div className="l-columns"> 235 <div className="chart-group l-columns__left"> 236 { graphComponents } 237 </div> 238 <div className="l-columns__right"> 239 <Alerts /> 240 <ClusterSummaryBar nodesSummary={this.props.nodesSummary} nodeSources={nodeSources} /> 241 </div> 242 </div> 243 </section> 244 </div> 245 ); 246 } 247 } 248 249 export default withRouter(connect( 250 (state: AdminUIState) => { 251 return { 252 nodesSummary: nodesSummarySelector(state), 253 nodesQueryValid: state.cachedData.nodes.valid, 254 livenessQueryValid: state.cachedData.nodes.valid, 255 hoverState: hoverStateSelector(state), 256 }; 257 }, 258 { 259 refreshNodes, 260 refreshLiveness, 261 hoverOn, 262 hoverOff, 263 }, 264 )(NodeGraphs));