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 };