github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/shared/containers/metricDataProvider/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 Long from "long"; 13 import moment from "moment"; 14 import React from "react"; 15 import { connect } from "react-redux"; 16 import { createSelector } from "reselect"; 17 import * as protos from "src/js/protos"; 18 import { MetricsQuery, requestMetrics as requestMetricsAction } from "src/redux/metrics"; 19 import { AdminUIState } from "src/redux/state"; 20 import { MilliToNano } from "src/util/convert"; 21 import { findChildrenOfType } from "src/util/find"; 22 import { Metric, MetricProps, MetricsDataComponentProps, QueryTimeInfo } from "src/views/shared/components/metricQuery"; 23 24 /** 25 * queryFromProps is a helper method which generates a TimeSeries Query data 26 * structure based on a MetricProps object. 27 */ 28 function queryFromProps( 29 metricProps: MetricProps, 30 graphProps: MetricsDataComponentProps, 31 ): protos.cockroach.ts.tspb.IQuery { 32 let derivative = protos.cockroach.ts.tspb.TimeSeriesQueryDerivative.NONE; 33 let sourceAggregator = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.SUM; 34 let downsampler = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.AVG; 35 36 // Compute derivative function. 37 if (!_.isNil(metricProps.derivative)) { 38 derivative = metricProps.derivative; 39 } else if (metricProps.rate) { 40 derivative = protos.cockroach.ts.tspb.TimeSeriesQueryDerivative.DERIVATIVE; 41 } else if (metricProps.nonNegativeRate) { 42 derivative = protos.cockroach.ts.tspb.TimeSeriesQueryDerivative.NON_NEGATIVE_DERIVATIVE; 43 } 44 // Compute downsample function. 45 if (!_.isNil(metricProps.downsampler)) { 46 downsampler = metricProps.downsampler; 47 } else if (metricProps.downsampleMax) { 48 downsampler = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.MAX; 49 } else if (metricProps.downsampleMin) { 50 downsampler = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.MIN; 51 } 52 // Compute aggregation function. 53 if (!_.isNil(metricProps.aggregator)) { 54 sourceAggregator = metricProps.aggregator; 55 } else if (metricProps.aggregateMax) { 56 sourceAggregator = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.MAX; 57 } else if (metricProps.aggregateMin) { 58 sourceAggregator = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.MIN; 59 } else if (metricProps.aggregateAvg) { 60 sourceAggregator = protos.cockroach.ts.tspb.TimeSeriesQueryAggregator.AVG; 61 } 62 63 return { 64 name: metricProps.name, 65 sources: metricProps.sources || graphProps.sources || undefined, 66 downsampler: downsampler, 67 source_aggregator: sourceAggregator, 68 derivative: derivative, 69 }; 70 } 71 72 /** 73 * MetricsDataProviderConnectProps are the properties provided to a 74 * MetricsDataProvider via the react-redux connect() system. 75 */ 76 interface MetricsDataProviderConnectProps { 77 metrics: MetricsQuery; 78 timeInfo: QueryTimeInfo; 79 requestMetrics: typeof requestMetricsAction; 80 } 81 82 /** 83 * MetricsDataProviderExplicitProps are the properties provided explicitly to a 84 * MetricsDataProvider object via React (i.e. setting an attribute in JSX). 85 */ 86 interface MetricsDataProviderExplicitProps { 87 id: string; 88 // If current is true, uses the current time instead of the global timewindow. 89 current?: boolean; 90 children?: React.ReactElement<{}>; 91 } 92 93 /** 94 * MetricsDataProviderProps is the complete set of properties which can be 95 * provided to a MetricsDataProvider. 96 */ 97 type MetricsDataProviderProps = MetricsDataProviderConnectProps & MetricsDataProviderExplicitProps; 98 99 /** 100 * MetricsDataProvider is a container which manages query data for a renderable 101 * component. For example, MetricsDataProvider may contain a "LineGraph" 102 * component; the metric set becomes responsible for querying the server 103 * required by that LineGraph. 104 * 105 * <MetricsDataProvider id="series-x-graph"> 106 * <LineGraph data="[]"> 107 * <Axis label="Series X over time."> 108 * <Metric title="" name="series.x" sources="node.1" /> 109 * </Axis> 110 * </LineGraph> 111 * </MetricsDataProvider>; 112 * 113 * Each MetricsDataProvider must have an ID field, which identifies this 114 * particular set of metrics to the metrics query reducer. Currently queries 115 * metrics from the reducer will be provided to the metric set via the 116 * react-redux connection. 117 * 118 * Additionally, each MetricsDataProvider has a single, externally set TimeSpan 119 * property, that determines the window over which time series should be 120 * queried. This property is also currently intended to be set via react-redux. 121 */ 122 class MetricsDataProvider extends React.Component<MetricsDataProviderProps, {}> { 123 private queriesSelector = createSelector( 124 ({ children }: MetricsDataProviderProps) => children, 125 (children) => { 126 // MetricsDataProvider should contain only one direct child. 127 const child: React.ReactElement<MetricsDataComponentProps> = React.Children.only(this.props.children); 128 // Perform a simple DFS to find all children which are Metric objects. 129 const selectors: React.ReactElement<MetricProps>[] = findChildrenOfType(children, Metric); 130 // Construct a query for each found selector child. 131 return _.map(selectors, (s) => queryFromProps(s.props, child.props)); 132 }); 133 134 private requestMessage = createSelector( 135 (props: MetricsDataProviderProps) => props.timeInfo, 136 this.queriesSelector, 137 (timeInfo, queries) => { 138 if (!timeInfo) { 139 return undefined; 140 } 141 return new protos.cockroach.ts.tspb.TimeSeriesQueryRequest({ 142 start_nanos: timeInfo.start, 143 end_nanos: timeInfo.end, 144 sample_nanos: timeInfo.sampleDuration, 145 queries, 146 }); 147 }); 148 149 /** 150 * Refresh nodes status query when props are changed; this will immediately 151 * trigger a new request if the previous query is no longer valid. 152 */ 153 refreshMetricsIfStale(props: MetricsDataProviderProps) { 154 const request = this.requestMessage(props); 155 if (!request) { 156 return; 157 } 158 const { metrics, requestMetrics, id } = props; 159 const nextRequest = metrics && metrics.nextRequest; 160 if (!nextRequest || !_.isEqual(nextRequest, request)) { 161 requestMetrics(id, request); 162 } 163 } 164 165 componentDidMount() { 166 // Refresh nodes status query when mounting. 167 this.refreshMetricsIfStale(this.props); 168 } 169 170 componentDidUpdate() { 171 // Refresh nodes status query when props are received; this will immediately 172 // trigger a new request if previous results are invalidated. 173 this.refreshMetricsIfStale(this.props); 174 } 175 176 getData() { 177 if (this.props.metrics) { 178 const { data, request } = this.props.metrics; 179 // Do not attach data if queries are not equivalent. 180 if (data && request && _.isEqual(request.queries, this.requestMessage(this.props).queries)) { 181 return data; 182 } 183 } 184 return undefined; 185 } 186 187 render() { 188 // MetricsDataProvider should contain only one direct child. 189 const child = React.Children.only(this.props.children); 190 const dataProps: MetricsDataComponentProps = { 191 data: this.getData(), 192 timeInfo: this.props.timeInfo, 193 }; 194 return React.cloneElement(child as React.ReactElement<MetricsDataComponentProps>, dataProps); 195 } 196 } 197 198 // timeInfoSelector converts the current global time window into a set of Long 199 // timestamps, which can be sent with requests to the server. 200 const timeInfoSelector = createSelector( 201 (state: AdminUIState) => state.timewindow, 202 (tw) => { 203 if (!_.isObject(tw.currentWindow)) { 204 return null; 205 } 206 return { 207 start: Long.fromNumber(MilliToNano(tw.currentWindow.start.valueOf())), 208 end: Long.fromNumber(MilliToNano(tw.currentWindow.end.valueOf())), 209 sampleDuration: Long.fromNumber(MilliToNano(tw.scale.sampleSize.asMilliseconds())), 210 }; 211 }); 212 213 const current = () => { 214 let now = moment(); 215 // Round to the nearest 10 seconds. There are 10000 ms in 10 s. 216 now = moment(Math.floor(now.valueOf() / 10000) * 10000); 217 return { 218 start: Long.fromNumber(MilliToNano(now.clone().subtract(30, "s").valueOf())), 219 end: Long.fromNumber(MilliToNano(now.valueOf())), 220 sampleDuration: Long.fromNumber(MilliToNano(moment.duration(10, "s").asMilliseconds())), 221 }; 222 }; 223 224 // Connect the MetricsDataProvider class to redux state. 225 const metricsDataProviderConnected = connect( 226 (state: AdminUIState, ownProps: MetricsDataProviderExplicitProps) => { 227 228 return { 229 metrics: state.metrics.queries[ownProps.id], 230 timeInfo: ownProps.current ? current() : timeInfoSelector(state), 231 }; 232 }, 233 { 234 requestMetrics: requestMetricsAction, 235 }, 236 )(MetricsDataProvider); 237 238 export { MetricsDataProvider as MetricsDataProviderUnconnected, metricsDataProviderConnected as MetricsDataProvider };