From 8cd54c94e99e9221c0901d15bdcf74c51764c246 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 14:47:54 -0700 Subject: [PATCH] make value processing reusable --- .../src/components/Gauge/Gauge.test.tsx | 92 +-------------- .../grafana-ui/src/components/Gauge/Gauge.tsx | 77 ++----------- .../src/utils/valueProcessor.test.ts | 107 ++++++++++++++++++ .../grafana-ui/src/utils/valueProcessor.ts | 97 ++++++++++++++++ .../panel/gauge/DisplayValueEditor.tsx | 64 +++++++++++ public/app/plugins/panel/gauge/GaugePanel.tsx | 42 ++++--- .../plugins/panel/gauge/GaugePanelEditor.tsx | 13 ++- .../panel/gauge/SingleStatValueEditor.tsx | 49 +------- public/app/plugins/panel/gauge/types.ts | 22 ++-- 9 files changed, 342 insertions(+), 221 deletions(-) create mode 100644 packages/grafana-ui/src/utils/valueProcessor.test.ts create mode 100644 packages/grafana-ui/src/utils/valueProcessor.ts create mode 100644 public/app/plugins/panel/gauge/DisplayValueEditor.tsx diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 70e29abc221..c6a49eb5b55 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Gauge, Props } from './Gauge'; -import { ValueMapping, MappingType } from '../../types'; import { getTheme } from '../../themes'; jest.mock('jquery', () => ({ @@ -12,19 +11,16 @@ jest.mock('jquery', () => ({ const setup = (propOverrides?: object) => { const props: Props = { maxValue: 100, - valueMappings: [], minValue: 0, - prefix: '', showThresholdMarkers: true, showThresholdLabels: false, - suffix: '', thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], - unit: 'none', - stat: 'avg', height: 300, width: 300, - value: 25, - decimals: 0, + value: { + text: '25', + numeric: 25, + }, theme: getTheme(), }; @@ -39,38 +35,6 @@ const setup = (propOverrides?: object) => { }; }; -describe('Get font color', () => { - it('should get first threshold color when only one threshold', () => { - const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); - - expect(instance.getFontColor(49)).toEqual('#7EB26D'); - }); - - it('should get the threshold color if value is same as a threshold', () => { - const { instance } = setup({ - thresholds: [ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, - ], - }); - - expect(instance.getFontColor(50)).toEqual('#EAB839'); - }); - - it('should get the nearest threshold color between thresholds', () => { - const { instance } = setup({ - thresholds: [ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, - { index: 0, value: -Infinity, color: '#7EB26D' }, - ], - }); - - expect(instance.getFontColor(55)).toEqual('#EAB839'); - }); -}); - describe('Get thresholds formatted', () => { it('should return first thresholds color for min and max', () => { const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); @@ -98,51 +62,3 @@ describe('Get thresholds formatted', () => { ]); }); }); - -describe('Format value', () => { - it('should return if value isNaN', () => { - const valueMappings: ValueMapping[] = []; - const value = 'N/A'; - const { instance } = setup({ valueMappings }); - - const result = instance.formatValue(value); - - expect(result).toEqual('N/A'); - }); - - it('should return formatted value if there are no value mappings', () => { - const valueMappings: ValueMapping[] = []; - const value = '6'; - const { instance } = setup({ valueMappings, decimals: 1 }); - - const result = instance.formatValue(value); - - expect(result).toEqual('6.0'); - }); - - it('should return formatted value if there are no matching value mappings', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings, decimals: 1 }); - - const result = instance.formatValue(value); - - expect(result).toEqual('10.0'); - }); - - it('should return mapped value if there are matching value mappings', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = '11'; - const { instance } = setup({ valueMappings, decimals: 1 }); - - const result = instance.formatValue(value); - - expect(result).toEqual('1-20'); - }); -}); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index d04daae3dab..460547a4d7e 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,28 +1,20 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { getMappedValue } from '../../utils/valueMappings'; import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; import { Themeable, GrafanaThemeType } from '../../types/theme'; -import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel'; -import { getValueFormat } from '../../utils/valueFormats/valueFormats'; - -type TimeSeriesValue = string | number | null; +import { Threshold, BasicGaugeColor } from '../../types/panel'; +import { DisplayValue } from '../../utils/valueProcessor'; export interface Props extends Themeable { - decimals?: number | null; + width: number; height: number; - valueMappings: ValueMapping[]; maxValue: number; minValue: number; - prefix: string; thresholds: Threshold[]; showThresholdMarkers: boolean; showThresholdLabels: boolean; - stat: string; - suffix: string; - unit: string; - width: number; - value: number; + + value: DisplayValue; } const FONT_SCALE = 1; @@ -32,15 +24,10 @@ export class Gauge extends PureComponent { static defaultProps = { maxValue: 100, - valueMappings: [], minValue: 0, - prefix: '', showThresholdMarkers: true, showThresholdLabels: false, - suffix: '', thresholds: [], - unit: 'none', - stat: 'avg', theme: GrafanaThemeType.Dark, }; @@ -52,49 +39,6 @@ export class Gauge extends PureComponent { this.draw(); } - formatValue(value: TimeSeriesValue) { - const { decimals, valueMappings, prefix, suffix, unit } = this.props; - - if (isNaN(value as number)) { - return value; - } - - if (valueMappings.length > 0) { - const valueMappedValue = getMappedValue(valueMappings, value); - if (valueMappedValue) { - return `${prefix && prefix + ' '}${valueMappedValue.text}${suffix && ' ' + suffix}`; - } - } - - const formatFunc = getValueFormat(unit); - const formattedValue = formatFunc(value as number, decimals); - const handleNoValueValue = formattedValue || 'no value'; - - return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`; - } - - getFontColor(value: TimeSeriesValue) { - const { thresholds, theme } = this.props; - - if (thresholds.length === 1) { - return getColorFromHexRgbOrName(thresholds[0].color, theme.type); - } - - const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; - if (atThreshold) { - return getColorFromHexRgbOrName(atThreshold.color, theme.type); - } - - const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); - - if (belowThreshold.length > 0) { - const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; - return getColorFromHexRgbOrName(nearestThreshold.color, theme.type); - } - - return BasicGaugeColor.Red; - } - getFormattedThresholds() { const { maxValue, minValue, thresholds, theme } = this.props; @@ -123,15 +67,13 @@ export class Gauge extends PureComponent { draw() { const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props; - const formattedValue = this.formatValue(value) as string; const dimension = Math.min(width, height * 1.3); const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio; const thresholdMarkersWidth = gaugeWidth / 5; - const fontSize = - Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1); + const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length); const thresholdLabelFontSize = fontSize / 2.5; const options = { @@ -160,9 +102,9 @@ export class Gauge extends PureComponent { width: thresholdMarkersWidth, }, value: { - color: this.getFontColor(value), + color: value.color ? value.color : BasicGaugeColor.Red, formatter: () => { - return formattedValue; + return value.text; }, font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' }, }, @@ -171,7 +113,8 @@ export class Gauge extends PureComponent { }, }; - const plotSeries = { data: [[0, value]] }; + const numeric = value.numeric !== null ? value.numeric : 0; + const plotSeries = { data: [[0, numeric]] }; try { $.plot(this.canvasElement, [plotSeries], options); diff --git a/packages/grafana-ui/src/utils/valueProcessor.test.ts b/packages/grafana-ui/src/utils/valueProcessor.test.ts new file mode 100644 index 00000000000..76c18f9e93c --- /dev/null +++ b/packages/grafana-ui/src/utils/valueProcessor.test.ts @@ -0,0 +1,107 @@ +import { getValueProcessor, getColorFromThreshold } from './valueProcessor'; +import { getTheme } from '../themes/index'; +import { GrafanaThemeType } from '../types/theme'; +import { MappingType, ValueMapping } from '../types/panel'; + +describe('Process values', () => { + const basicConversions = [ + { value: null, text: '' }, + { value: undefined, text: '' }, + { value: 1.23, text: '1.23' }, + { value: 1, text: '1' }, + { value: 'hello', text: 'hello' }, + { value: {}, text: '[object Object]' }, + { value: [], text: '' }, + { value: [1, 2, 3], text: '1,2,3' }, + { value: ['a', 'b', 'c'], text: 'a,b,c' }, + ]; + + it('should return return a string for any input value', () => { + const processor = getValueProcessor(); + basicConversions.forEach(item => { + expect(processor(item.value).text).toBe(item.text); + }); + }); + + it('should add a suffix to any value', () => { + const processor = getValueProcessor({ + prefix: 'xxx', + theme: getTheme(GrafanaThemeType.Dark), + }); + basicConversions.forEach(item => { + expect(processor(item.value).text).toBe('xxx' + item.text); + }); + }); +}); + +describe('Get color from threshold', () => { + it('should get first threshold color when only one threshold', () => { + const thresholds = [{ index: 0, value: -Infinity, color: '#7EB26D' }]; + expect(getColorFromThreshold(49, thresholds)).toEqual('#7EB26D'); + }); + + it('should get the threshold color if value is same as a threshold', () => { + const thresholds = [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]; + expect(getColorFromThreshold(50, thresholds)).toEqual('#EAB839'); + }); + + it('should get the nearest threshold color between thresholds', () => { + const thresholds = [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]; + expect(getColorFromThreshold(55, thresholds)).toEqual('#EAB839'); + }); +}); + +describe('Format value', () => { + it('should return if value isNaN', () => { + const valueMappings: ValueMapping[] = []; + const value = 'N/A'; + const instance = getValueProcessor({ mappings: valueMappings }); + + const result = instance(value); + + expect(result.text).toEqual('N/A'); + }); + + it('should return formatted value if there are no value mappings', () => { + const valueMappings: ValueMapping[] = []; + const value = '6'; + + const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); + + const result = instance(value); + + expect(result.text).toEqual('6.0'); + }); + + it('should return formatted value if there are no matching value mappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, + ]; + const value = '10'; + const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); + + const result = instance(value); + + expect(result.text).toEqual('10.0'); + }); + + it('should return mapped value if there are matching value mappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = '11'; + const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); + + expect(instance(value).text).toEqual('1-20'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueProcessor.ts b/packages/grafana-ui/src/utils/valueProcessor.ts new file mode 100644 index 00000000000..243904c269c --- /dev/null +++ b/packages/grafana-ui/src/utils/valueProcessor.ts @@ -0,0 +1,97 @@ +import { ValueMapping, Threshold } from '../types/panel'; +import _ from 'lodash'; +import { getValueFormat, DecimalCount } from './valueFormats/valueFormats'; +import { getMappedValue } from './valueMappings'; +import { GrafanaTheme, GrafanaThemeType } from '../types/theme'; +import { getColorFromHexRgbOrName } from './namedColorsPalette'; + +export interface DisplayValue { + text: string; // How the value should be displayed + numeric?: number; // the value as a number + color?: string; // suggested color +} + +export interface DisplayValueOptions { + unit?: string; + decimals?: DecimalCount; + scaledDecimals?: DecimalCount; + isUtc?: boolean; + + color?: string; + mappings?: ValueMapping[]; + thresholds?: Threshold[]; + prefix?: string; + suffix?: string; + + noValue?: string; + theme?: GrafanaTheme; // Will pick 'dark' if not defined +} + +export type ValueProcessor = (value: any) => DisplayValue; + +export function getValueProcessor(options?: DisplayValueOptions): ValueProcessor { + if (options && !_.isEmpty(options)) { + const formatFunc = getValueFormat(options.unit || 'none'); + return (value: any) => { + const { prefix, suffix, mappings, thresholds, theme } = options; + let color = options.color; + + let text = _.toString(value); + const numeric = _.toNumber(value); + + if (mappings && mappings.length > 0) { + const mappedValue = getMappedValue(mappings, value); + if (mappedValue) { + text = mappedValue.text; + // TODO? convert the mapped value back to a number? + } + } + + if (_.isNumber(numeric)) { + text = formatFunc(numeric, options.decimals, options.scaledDecimals, options.isUtc); + if (thresholds && thresholds.length > 0) { + color = getColorFromThreshold(numeric, thresholds, theme); + } + } + + if (!text) { + text = options.noValue ? options.noValue : ''; + } + if (prefix) { + text = prefix + text; + } + if (suffix) { + text = text + suffix; + } + return { text, numeric, color }; + }; + } + return toStringProcessor; +} + +function toStringProcessor(value: any): DisplayValue { + return { text: _.toString(value), numeric: _.toNumber(value) }; +} + +export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string { + const themeType = theme ? theme.type : GrafanaThemeType.Dark; + + if (thresholds.length === 1) { + return getColorFromHexRgbOrName(thresholds[0].color, themeType); + } + + const atThreshold = thresholds.filter(threshold => value === threshold.value)[0]; + if (atThreshold) { + return getColorFromHexRgbOrName(atThreshold.color, themeType); + } + + const belowThreshold = thresholds.filter(threshold => value > threshold.value); + + if (belowThreshold.length > 0) { + const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; + return getColorFromHexRgbOrName(nearestThreshold.color, themeType); + } + + // Use the first threshold as the default color + return getColorFromHexRgbOrName(thresholds[0].color, themeType); +} diff --git a/public/app/plugins/panel/gauge/DisplayValueEditor.tsx b/public/app/plugins/panel/gauge/DisplayValueEditor.tsx new file mode 100644 index 00000000000..51c956a9529 --- /dev/null +++ b/public/app/plugins/panel/gauge/DisplayValueEditor.tsx @@ -0,0 +1,64 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Components +import { FormField, FormLabel, PanelOptionsGroup, UnitPicker } from '@grafana/ui'; + +// Types +import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor'; + +const labelWidth = 6; + +export interface Props { + options: DisplayValueOptions; + onChange: (options: DisplayValueOptions) => void; +} + +export class DisplayValueEditor extends PureComponent { + onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); + + onDecimalChange = event => { + if (!isNaN(event.target.value)) { + this.props.onChange({ + ...this.props.options, + decimals: parseInt(event.target.value, 10), + }); + } else { + this.props.onChange({ + ...this.props.options, + decimals: null, + }); + } + }; + + onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); + onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); + + render() { + const { unit, decimals, prefix, suffix } = this.props.options; + + let decimalsString = ''; + if (Number.isFinite(decimals)) { + decimalsString = decimals.toString(); + } + + return ( + +
+ Unit + +
+ + + +
+ ); + } +} diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index b75d4a1c7f3..425ccb00356 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -9,30 +9,50 @@ import { Gauge } from '@grafana/ui'; // Types import { GaugeOptions } from './types'; -import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types'; +import { PanelProps, NullValueMode, BasicGaugeColor } from '@grafana/ui/src/types'; +import { DisplayValue, getValueProcessor } from '@grafana/ui/src/utils/valueProcessor'; interface Props extends PanelProps {} interface State { - value: TimeSeriesValue; + value: DisplayValue; } export class GaugePanel extends Component { constructor(props: Props) { super(props); + + if (props.options.valueOptions) { + console.warn('TODO!! how do we best migration options?'); + } + this.state = { - value: this.findValue(props), + value: this.findDisplayValue(props), }; } componentDidUpdate(prevProps: Props) { if (this.props.panelData !== prevProps.panelData) { - this.setState({ value: this.findValue(this.props) }); + this.setState({ value: this.findDisplayValue(this.props) }); } } + findDisplayValue(props: Props): DisplayValue { + const { replaceVariables, options } = this.props; + const { displayOptions } = options; + + const prefix = replaceVariables(displayOptions.prefix); + const suffix = replaceVariables(displayOptions.suffix); + return getValueProcessor({ + color: BasicGaugeColor.Red, // The default color + ...displayOptions, + prefix, + suffix, + // ??? theme:getTheme(GrafanaThemeType.Dark), !! how do I get it here??? + })(this.findValue(props)); + } + findValue(props: Props): number | null { const { panelData, options } = props; - const { valueOptions } = options; if (panelData.timeSeries) { const vmSeries = processTimeSeries({ @@ -41,7 +61,7 @@ export class GaugePanel extends Component { }); if (vmSeries[0]) { - return vmSeries[0].stats[valueOptions.stat]; + return vmSeries[0].stats[options.stat]; } } else if (panelData.tableData) { return panelData.tableData.rows[0].find(prop => prop > 0); @@ -50,12 +70,9 @@ export class GaugePanel extends Component { } render() { - const { width, height, replaceVariables, options } = this.props; - const { valueOptions } = options; + const { width, height, options } = this.props; const { value } = this.state; - const prefix = replaceVariables(valueOptions.prefix); - const suffix = replaceVariables(valueOptions.suffix); return ( {theme => ( @@ -63,12 +80,7 @@ export class GaugePanel extends Component { value={value} width={width} height={height} - prefix={prefix} - suffix={suffix} - unit={valueOptions.unit} - decimals={valueOptions.decimals} thresholds={options.thresholds} - valueMappings={options.valueMappings} showThresholdLabels={options.showThresholdLabels} showThresholdMarkers={options.showThresholdMarkers} minValue={options.minValue} diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx index f226be7328c..55a0377848d 100644 --- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -11,6 +11,8 @@ import { import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; import { GaugeOptionsBox } from './GaugeOptionsBox'; import { GaugeOptions, SingleStatValueOptions } from './types'; +import { DisplayValueEditor } from './DisplayValueEditor'; +import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor'; export class GaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => @@ -31,13 +33,22 @@ export class GaugePanelEditor extends PureComponent + this.props.onOptionsChange({ + ...this.props.options, + displayOptions, + }); + render() { const { onOptionsChange, options } = this.props; return ( <> - + {/* This just sets the 'stats', that should be moved to somethign more general */} + + + diff --git a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx index e711df6a2d3..414a606b108 100644 --- a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx +++ b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx @@ -2,10 +2,10 @@ import React, { PureComponent } from 'react'; // Components -import { FormField, FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui'; +import { FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; // Types -import { SingleStatValueOptions } from './types'; +import { GaugeOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -24,41 +24,18 @@ const statOptions = [ const labelWidth = 6; export interface Props { - options: SingleStatValueOptions; - onChange: (valueOptions: SingleStatValueOptions) => void; + options: GaugeOptions; + onChange: (options: GaugeOptions) => void; } export class SingleStatValueEditor extends PureComponent { - onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); - onDecimalChange = event => { - if (!isNaN(event.target.value)) { - this.props.onChange({ - ...this.props.options, - decimals: parseInt(event.target.value, 10), - }); - } else { - this.props.onChange({ - ...this.props.options, - decimals: null, - }); - } - }; - - onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); - onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); - render() { - const { stat, unit, decimals, prefix, suffix } = this.props.options; - - let decimalsString = ''; - if (Number.isFinite(decimals)) { - decimalsString = decimals.toString(); - } + const { stat } = this.props.options; return ( - +
Stat