github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/shared/components/summaryBar/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 classNames from "classnames";
    14  import * as protos from "src/js/protos";
    15  
    16  import "./summarybar.styl";
    17  
    18  import { MetricsDataProvider } from "src/views/shared/containers/metricDataProvider";
    19  import { MetricsDataComponentProps } from "src/views/shared/components/metricQuery";
    20  import { ToolTipWrapper } from "src/views/shared/components/toolTip";
    21  type TSResponse = protos.cockroach.ts.tspb.TimeSeriesQueryResponse;
    22  
    23  export enum SummaryMetricsAggregator {
    24    FIRST = 1,
    25    SUM = 2,
    26  }
    27  
    28  interface SummaryValueProps {
    29    title: React.ReactNode;
    30    value: React.ReactNode;
    31    classModifier?: string;
    32  }
    33  
    34  interface SummaryStatProps {
    35    title: React.ReactNode;
    36    value?: number;
    37    format?: (n: number) => string;
    38    aggregator?: SummaryMetricsAggregator;
    39  }
    40  
    41  interface SummaryHeadlineStatProps extends SummaryStatProps {
    42    tooltip?: string;
    43  }
    44  
    45  interface SummaryStatMessageProps {
    46    message: string;
    47  }
    48  
    49  interface SummaryStatBreakdownProps {
    50    title: React.ReactNode;
    51    tooltip?: string;
    52    value?: number;
    53    format?: (i: number) => string;
    54    modifier?: "dead" | "suspect" | "healthy";
    55  }
    56  
    57  function numberToString(n: number) {
    58    return n.toString();
    59  }
    60  
    61  export function formatNumberForDisplay(value: number, format: (n: number) => string = numberToString) {
    62    if (!_.isNumber(value)) {
    63      return "-";
    64    }
    65    return format(value);
    66  }
    67  
    68  /**
    69   * SummaryBar is a simple component backing a common motif in our UI - a
    70   * collection of summarized statistics.
    71   */
    72  export function SummaryBar(props: { children?: React.ReactNode }) {
    73    return <div className="summary-section">
    74      { props.children }
    75    </div>;
    76  }
    77  
    78  /**
    79   * SummaryValue places a single labeled value onto a summary bar; this
    80   * consists of a label and a formatted value. Summary stats are visually
    81   * separated from other summary stats. A summary stat can contain children, such
    82   * as messages and breakdowns.
    83   */
    84  export function SummaryValue(props: SummaryValueProps & {children?: React.ReactNode}) {
    85    const topClasses = classNames(
    86      "summary-stat",
    87      props.classModifier ? `summary-stat--${props.classModifier}` : null,
    88    );
    89    return (
    90      <div className={topClasses}>
    91        <div className="summary-stat__body">
    92          <span className="summary-stat__title">
    93            { props.title }
    94          </span>
    95          <span className="summary-stat__value">
    96            { props.value }
    97          </span>
    98        </div>
    99        { props.children }
   100      </div>
   101    );
   102  }
   103  
   104  /**
   105   * SummaryStat is a convenience component for SummaryValues where the value
   106   * consists of a single formatted number; it automatically handles cases where
   107   * the value is a non-numeric value and applies an appearance modifier specific
   108   * to numeric values.
   109   */
   110  export function SummaryStat(props: SummaryStatProps & {children?: React.ReactNode}) {
   111    return (
   112      <SummaryValue
   113        title={props.title}
   114        value={formatNumberForDisplay(props.value, props.format)}
   115        classModifier="number"
   116      >
   117        {props.children}
   118      </SummaryValue>
   119    );
   120  }
   121  
   122  /**
   123   * SummaryLabel places a label onto a SummaryBar without a corresponding
   124   * statistic. This can be used to label a section of the bar.
   125   */
   126  export function SummaryLabel(props: {children?: React.ReactNode}) {
   127    return <div className="summary-label">
   128      { props.children }
   129    </div>;
   130  }
   131  
   132  /**
   133   * SummaryStatMessage can be placed inside of a SummaryStat to provide visible
   134   * descriptive information about that statistic.
   135   */
   136  export function SummaryStatMessage(props: SummaryStatMessageProps & {children?: React.ReactNode}) {
   137    return <span className="summary-stat__tooltip">{ props.message }</span>;
   138  }
   139  
   140  /**
   141   * SummaryStatBreakdown can be placed inside of a SummaryStat to provide
   142   * a detailed breakdown of the main statistic. Each breakdown contains a label
   143   * and numeric statistic.
   144   */
   145  export function SummaryStatBreakdown(props: SummaryStatBreakdownProps & {children?: React.ReactNode}) {
   146    const modifierClass = props.modifier ? `summary-stat-breakdown--${props.modifier}` : null;
   147    return <div className={classNames("summary-stat-breakdown", modifierClass)}>
   148      <div className="summary-stat-breakdown__body">
   149        <span className="summary-stat-breakdown__title">
   150          { props.title }
   151        </span>
   152        <span className="summary-stat-breakdown__value">
   153          { formatNumberForDisplay(props.value, props.format) }
   154        </span>
   155      </div>
   156    </div>;
   157  }
   158  
   159  /**
   160   * SummaryMetricStat is a helpful component that creates a SummaryStat where
   161   * metric data is automatically derived from a metric component.
   162   */
   163  export function SummaryMetricStat(propsWithID: SummaryStatProps & { id: string, summaryStatMessage?: string } & { children?: React.ReactNode }) {
   164    const { id, ...props } = propsWithID;
   165    return <MetricsDataProvider current id={id} >
   166      <SummaryMetricStatHelper {...props} />
   167    </MetricsDataProvider>;
   168  }
   169  
   170  function SummaryMetricStatHelper(props: MetricsDataComponentProps & SummaryStatProps & { summaryStatMessage?: string } & { children?: React.ReactNode}) {
   171    const value = aggregateLatestValuesFromMetrics(props.data, props.aggregator);
   172    const {title, format, summaryStatMessage} = props;
   173    return (
   174      <SummaryStat
   175        title={title}
   176        format={format}
   177        value={_.isNumber(value) ? value : props.value}
   178      >
   179        {summaryStatMessage &&
   180          <SummaryStatMessage message={summaryStatMessage}/>
   181        }
   182      </SummaryStat>
   183    );
   184  }
   185  
   186  function aggregateLatestValuesFromMetrics(data?: TSResponse, aggregator?: SummaryMetricsAggregator) {
   187    if (!data || !data.results || !data.results.length) {
   188      return null;
   189    }
   190  
   191    const latestValues = data.results.map(({ datapoints }) => {
   192      return datapoints && datapoints.length && _.last(datapoints).value;
   193    });
   194  
   195    if (aggregator) {
   196      switch (aggregator) {
   197        case SummaryMetricsAggregator.SUM:
   198          return _.sum(latestValues);
   199        case SummaryMetricsAggregator.FIRST:
   200        default:
   201          // Do nothing, which does default action (below) of
   202          // returning the first metric.
   203          break;
   204      }
   205    }
   206    // Return first metric.
   207    return latestValues[0];
   208  }
   209  /**
   210   * SummaryHeadlineStat is similar to a normal SummaryStat, but is visually laid
   211   * out to draw attention to the numerical statistic.
   212   */
   213  export class SummaryHeadlineStat extends React.Component<SummaryHeadlineStatProps, {}> {
   214    render() {
   215      return <div className="summary-headline">
   216        <div className="summary-headline__value">{formatNumberForDisplay(this.props.value, this.props.format)}</div>
   217        <div className="summary-headline__title">
   218          {this.props.title}
   219          <div className="section-heading__tooltip">
   220            <ToolTipWrapper text={this.props.tooltip}>
   221              <div className="section-heading__tooltip-hover-area">
   222                <div className="section-heading__info-icon">i</div>
   223              </div>
   224            </ToolTipWrapper>
   225          </div>
   226        </div>
   227      </div>;
   228    }
   229  }