OptionsUI: use NumberInput for number options (#46046)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Adela Almasan 2022-06-14 15:42:26 -05:00 committed by GitHub
parent d61d439b11
commit 1af63ba5f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 158 deletions

View File

@ -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<Props, State> {
state: State = { text: '', inputCorrected: false };
inputRef = React.createRef<HTMLInputElement>();
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.updateValue();
}
};
renderInput() {
return (
<Input
type="number"
ref={this.inputRef}
min={this.props.min}
max={this.props.max}
step={this.props.step}
autoFocus={this.props.autoFocus}
value={this.state.text}
onChange={this.onChange}
onBlur={this.updateValue}
onKeyPress={this.onKeyPress}
placeholder={this.props.placeholder}
/>
);
}
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 (
<Field invalid={inputCorrected} error={`Value out of range ${range} `}>
{this.renderInput()}
</Field>
);
}
return this.renderInput();
}
}

View File

@ -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<FieldConfigEditorProps<number, NumberFieldConfigSettings>> = ({
value,
@ -16,41 +12,20 @@ export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFi
const { settings } = item;
const onValueChange = useCallback(
(e: React.SyntheticEvent) => {
if (e.hasOwnProperty('key')) {
// handling keyboard event
const evt = e as React.KeyboardEvent<HTMLInputElement>;
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<HTMLInputElement>;
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 (
<Input
defaultValue={defaultValue}
<NumberInput
value={value}
min={settings?.min}
max={settings?.max}
type="number"
step={settings?.step}
placeholder={settings?.placeholder}
onBlur={onValueChange}
onKeyDown={onValueChange}
onChange={onValueChange}
/>
);
};

View File

@ -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<Props, State> {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.onBlur(e as any);
}
};
render() {
const { placeholder } = this.props;
const { text, inputCorrected } = this.state;
return (
<Field invalid={inputCorrected} error={inputCorrected ? 'Cannot go beyond range' : ''}>
<Input
type="number"
min={this.props.min}
max={this.props.max}
step={this.props.step}
autoFocus={this.props.autoFocus}
value={text}
onChange={this.onChange}
onBlur={this.onBlur}
onKeyPress={this.onKeyPress}
placeholder={placeholder}
/>
</Field>
);
}
}

View File

@ -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<string> = {
label: 'Fixed value',
value: '_____fixed_____',

View File

@ -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<string> = {
label: 'Fixed value',
value: '_____fixed_____',

View File

@ -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<TransformerUIProps<HistogramTransformerOptions>> = ({
input,
@ -15,22 +16,20 @@ export const HistogramTransformerEditor: React.FC<TransformerUIProps<HistogramTr
const labelWidth = 18;
const onBucketSizeChanged = useCallback(
(evt: FormEvent<HTMLInputElement>) => {
const val = evt.currentTarget.valueAsNumber;
(val?: number) => {
onChange({
...options,
bucketSize: isNaN(val) ? undefined : val,
bucketSize: val,
});
},
[onChange, options]
);
const onBucketOffsetChanged = useCallback(
(evt: FormEvent<HTMLInputElement>) => {
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<TransformerUIProps<HistogramTr
label={histogramFieldInfo.bucketSize.name}
tooltip={histogramFieldInfo.bucketSize.description}
>
<Input type="number" value={options.bucketSize} placeholder="auto" onChange={onBucketSizeChanged} min={0} />
<NumberInput value={options.bucketSize} placeholder="auto" onChange={onBucketSizeChanged} min={0} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
@ -60,13 +59,7 @@ export const HistogramTransformerEditor: React.FC<TransformerUIProps<HistogramTr
label={histogramFieldInfo.bucketOffset.name}
tooltip={histogramFieldInfo.bucketOffset.description}
>
<Input
type="number"
value={options.bucketOffset}
placeholder="none"
onChange={onBucketOffsetChanged}
min={0}
/>
<NumberInput value={options.bucketOffset} placeholder="none" onChange={onBucketOffsetChanged} min={0} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>

View File

@ -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';

View File

@ -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';

View File

@ -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';