From 1af63ba5f97625c85b3d191a9394805bcd0739ff Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 14 Jun 2022 15:42:26 -0500 Subject: [PATCH] OptionsUI: use NumberInput for number options (#46046) Co-authored-by: Ryan McKinley --- .../core/components/OptionsUI/NumberInput.tsx | 137 ++++++++++++++++++ .../app/core/components/OptionsUI/number.tsx | 41 +----- .../dimensions/editors/NumberInput.tsx | 102 ------------- .../editors/ScalarDimensionEditor.tsx | 3 +- .../editors/ScaleDimensionEditor.tsx | 3 +- .../editors/HistogramTransformerEditor.tsx | 25 ++-- .../panel/canvas/editor/PlacementEditor.tsx | 2 +- .../panel/geomap/editor/MapViewEditor.tsx | 2 +- .../panel/geomap/editor/StyleRuleEditor.tsx | 2 +- 9 files changed, 159 insertions(+), 158 deletions(-) create mode 100644 public/app/core/components/OptionsUI/NumberInput.tsx delete mode 100644 public/app/features/dimensions/editors/NumberInput.tsx diff --git a/public/app/core/components/OptionsUI/NumberInput.tsx b/public/app/core/components/OptionsUI/NumberInput.tsx new file mode 100644 index 00000000000..5a95524cb44 --- /dev/null +++ b/public/app/core/components/OptionsUI/NumberInput.tsx @@ -0,0 +1,137 @@ +import { debounce } from 'lodash'; +import React, { PureComponent } from 'react'; + +import { Field, Input } from '@grafana/ui'; + +interface Props { + value?: number; + placeholder?: string; + autoFocus?: boolean; + onChange: (number?: number) => void; + min?: number; + max?: number; + step?: number; +} + +interface State { + text: string; + inputCorrected: boolean; +} + +/** + * This is an Input field that will call `onChange` for blur and enter + * + * @internal this is not exported to the `@grafana/ui` library, it is used + * by options editor (number and slider), and direclty with in grafana core + */ + +export class NumberInput extends PureComponent { + state: State = { text: '', inputCorrected: false }; + inputRef = React.createRef(); + + componentDidMount() { + this.setState({ + text: isNaN(this.props.value!) ? '' : `${this.props.value}`, + }); + } + + componentDidUpdate(oldProps: Props) { + if (this.props.value !== oldProps.value) { + const text = isNaN(this.props.value!) ? '' : `${this.props.value}`; + if (text !== this.state.text) { + this.setState({ text }); + } + } + } + + updateValue = () => { + let value: number | undefined = undefined; + const txt = this.inputRef.current?.value; + if (txt?.length) { + value = +txt; + if (isNaN(value)) { + return; + } + } + if (value !== this.props.value) { + this.props.onChange(value); + } + if (this.state.inputCorrected) { + this.setState({ inputCorrected: false }); + } + }; + + updateValueDebounced = debounce(this.updateValue, 500); // 1/2 second delay + + onChange = (e: React.FocusEvent) => { + let newValue: string | undefined = undefined; + let corrected = false; + const min = this.props.min; + const max = this.props.max; + const currValue = e.currentTarget.valueAsNumber; + if (!Number.isNaN(currValue)) { + if (min != null && currValue < min) { + newValue = min.toString(); + corrected = true; + } else if (max != null && currValue > max) { + newValue = max.toString(); + corrected = true; + } else { + newValue = e.currentTarget.value; + } + } + this.setState({ + text: newValue ? newValue : '', + inputCorrected: corrected, + }); + this.updateValueDebounced(); + }; + + onKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + this.updateValue(); + } + }; + + renderInput() { + return ( + + ); + } + + render() { + const { inputCorrected } = this.state; + if (inputCorrected) { + let range = ''; + let { min, max } = this.props; + if (max == null) { + if (min != null) { + range = `< ${min}`; + } + } else if (min != null) { + range = `${min} < > ${max}`; + } else { + range = `> ${max}`; + } + return ( + + {this.renderInput()} + + ); + } + + return this.renderInput(); + } +} diff --git a/public/app/core/components/OptionsUI/number.tsx b/public/app/core/components/OptionsUI/number.tsx index 444ebd7461f..a741ae6f4db 100644 --- a/public/app/core/components/OptionsUI/number.tsx +++ b/public/app/core/components/OptionsUI/number.tsx @@ -1,12 +1,8 @@ import React, { useCallback } from 'react'; -import { - FieldConfigEditorProps, - toIntegerOrUndefined, - toFloatOrUndefined, - NumberFieldConfigSettings, -} from '@grafana/data'; -import { Input } from '@grafana/ui'; +import { FieldConfigEditorProps, NumberFieldConfigSettings } from '@grafana/data'; + +import { NumberInput } from './NumberInput'; export const NumberValueEditor: React.FC> = ({ value, @@ -16,41 +12,20 @@ export const NumberValueEditor: React.FC { - if (e.hasOwnProperty('key')) { - // handling keyboard event - const evt = e as React.KeyboardEvent; - if (evt.key === 'Enter') { - onChange( - settings?.integer - ? toIntegerOrUndefined(evt.currentTarget.value) - : toFloatOrUndefined(evt.currentTarget.value) - ); - } - } else { - // handling form event - const evt = e as React.FormEvent; - onChange( - settings?.integer - ? toIntegerOrUndefined(evt.currentTarget.value) - : toFloatOrUndefined(evt.currentTarget.value) - ); - } + (value: number | undefined) => { + onChange(settings?.integer && value !== undefined ? Math.floor(value) : value); }, [onChange, settings?.integer] ); - const defaultValue = value === undefined || value === null || isNaN(value) ? '' : value.toString(); return ( - ); }; diff --git a/public/app/features/dimensions/editors/NumberInput.tsx b/public/app/features/dimensions/editors/NumberInput.tsx deleted file mode 100644 index 24e8b281c13..00000000000 --- a/public/app/features/dimensions/editors/NumberInput.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { Field, Input } from '@grafana/ui'; - -interface Props { - value?: number; - placeholder?: string; - autoFocus?: boolean; - onChange: (number?: number) => void; - min?: number; - max?: number; - step?: number; -} - -interface State { - text: string; - inputCorrected: boolean; -} - -/** - * This is an Input field that will call `onChange` for blur and enter - */ -export class NumberInput extends PureComponent { - state: State = { text: '', inputCorrected: false }; - - componentDidMount() { - this.setState({ - ...this.state, - text: isNaN(this.props.value!) ? '' : `${this.props.value}`, - }); - } - - componentDidUpdate(oldProps: Props) { - if (this.props.value !== oldProps.value) { - this.setState({ - ...this.state, - text: isNaN(this.props.value!) ? '' : `${this.props.value}`, - }); - } - } - - onBlur = (e: React.FocusEvent) => { - let value: number | undefined = undefined; - const txt = e.currentTarget.value; - if (txt && !isNaN(e.currentTarget.valueAsNumber)) { - value = e.currentTarget.valueAsNumber; - } - this.props.onChange(value); - this.setState({ ...this.state, inputCorrected: false }); - }; - - onChange = (e: React.FocusEvent) => { - let newValue: string | undefined = undefined; - let corrected = false; - const min = this.props.min; - const max = this.props.max; - const currValue = e.currentTarget.valueAsNumber; - if (!Number.isNaN(currValue)) { - if (min != null && currValue < min) { - newValue = min.toString(); - corrected = true; - } else if (max != null && currValue > max) { - newValue = max.toString(); - corrected = true; - } else { - newValue = e.currentTarget.value; - } - } - this.setState({ - ...this.state, - text: newValue ? newValue : '', - inputCorrected: corrected, - }); - }; - - onKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - this.onBlur(e as any); - } - }; - - render() { - const { placeholder } = this.props; - const { text, inputCorrected } = this.state; - return ( - - - - ); - } -} diff --git a/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx b/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx index e5c8ae475e6..7b0e306bee4 100644 --- a/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx @@ -4,11 +4,10 @@ import React, { FC, useCallback } from 'react'; import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data'; import { InlineField, InlineFieldRow, RadioButtonGroup, Select, useStyles2 } from '@grafana/ui'; import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/src/components/MatchersUI/utils'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { ScalarDimensionConfig, ScalarDimensionMode, ScalarDimensionOptions } from '../types'; -import { NumberInput } from './NumberInput'; - const fixedValueOption: SelectableValue = { label: 'Fixed value', value: '_____fixed_____', diff --git a/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx b/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx index 882d4b86e1c..679957b9d28 100644 --- a/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx @@ -3,6 +3,7 @@ import React, { FC, useCallback, useMemo } from 'react'; import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data'; import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { useFieldDisplayNames, @@ -11,8 +12,6 @@ import { import { validateScaleOptions, validateScaleConfig } from '../scale'; import { ScaleDimensionConfig, ScaleDimensionOptions } from '../types'; -import { NumberInput } from './NumberInput'; - const fixedValueOption: SelectableValue = { label: 'Fixed value', value: '_____fixed_____', diff --git a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx index 82982aa8dee..bac37e68d45 100644 --- a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx +++ b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx @@ -1,11 +1,12 @@ -import React, { FormEvent, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { DataTransformerID, standardTransformers, TransformerRegistryItem, TransformerUIProps } from '@grafana/data'; import { HistogramTransformerOptions, histogramFieldInfo, } from '@grafana/data/src/transformations/transformers/histogram'; -import { InlineField, InlineFieldRow, InlineSwitch, Input } from '@grafana/ui'; +import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; export const HistogramTransformerEditor: React.FC> = ({ input, @@ -15,22 +16,20 @@ export const HistogramTransformerEditor: React.FC) => { - const val = evt.currentTarget.valueAsNumber; + (val?: number) => { onChange({ ...options, - bucketSize: isNaN(val) ? undefined : val, + bucketSize: val, }); }, [onChange, options] ); const onBucketOffsetChanged = useCallback( - (evt: FormEvent) => { - const val = evt.currentTarget.valueAsNumber; + (val?: number) => { onChange({ ...options, - bucketOffset: isNaN(val) ? undefined : val, + bucketOffset: val, }); }, [onChange, options] @@ -51,7 +50,7 @@ export const HistogramTransformerEditor: React.FC - + @@ -60,13 +59,7 @@ export const HistogramTransformerEditor: React.FC - + diff --git a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx index 81cbc37e423..16e2b943c8e 100644 --- a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx @@ -4,8 +4,8 @@ import { Subject } from 'rxjs'; import { SelectableValue, StandardEditorProps } from '@grafana/data'; import { Field, HorizontalGroup, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { HorizontalConstraint, Placement, VerticalConstraint } from 'app/features/canvas'; -import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; import { PanelOptions } from '../models.gen'; diff --git a/public/app/plugins/panel/geomap/editor/MapViewEditor.tsx b/public/app/plugins/panel/geomap/editor/MapViewEditor.tsx index 4385d1745cd..8d4fff5d503 100644 --- a/public/app/plugins/panel/geomap/editor/MapViewEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/MapViewEditor.tsx @@ -3,7 +3,7 @@ import React, { FC, useMemo, useCallback } from 'react'; import { StandardEditorProps, SelectableValue } from '@grafana/data'; import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui'; -import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { GeomapInstanceState } from '../GeomapPanel'; import { GeomapPanelOptions, MapViewConfig } from '../types'; diff --git a/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx index f49afc22812..1a58a49d149 100644 --- a/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data'; import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui'; -import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { StyleEditor } from '../layers/data/StyleEditor'; import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';