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 }