mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
OptionsUI: use NumberInput for number options (#46046)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
d61d439b11
commit
1af63ba5f9
137
public/app/core/components/OptionsUI/NumberInput.tsx
Normal file
137
public/app/core/components/OptionsUI/NumberInput.tsx
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,8 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import {
|
import { FieldConfigEditorProps, NumberFieldConfigSettings } from '@grafana/data';
|
||||||
FieldConfigEditorProps,
|
|
||||||
toIntegerOrUndefined,
|
import { NumberInput } from './NumberInput';
|
||||||
toFloatOrUndefined,
|
|
||||||
NumberFieldConfigSettings,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { Input } from '@grafana/ui';
|
|
||||||
|
|
||||||
export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFieldConfigSettings>> = ({
|
export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFieldConfigSettings>> = ({
|
||||||
value,
|
value,
|
||||||
@ -16,41 +12,20 @@ export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFi
|
|||||||
const { settings } = item;
|
const { settings } = item;
|
||||||
|
|
||||||
const onValueChange = useCallback(
|
const onValueChange = useCallback(
|
||||||
(e: React.SyntheticEvent) => {
|
(value: number | undefined) => {
|
||||||
if (e.hasOwnProperty('key')) {
|
onChange(settings?.integer && value !== undefined ? Math.floor(value) : value);
|
||||||
// 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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[onChange, settings?.integer]
|
[onChange, settings?.integer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultValue = value === undefined || value === null || isNaN(value) ? '' : value.toString();
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<NumberInput
|
||||||
defaultValue={defaultValue}
|
value={value}
|
||||||
min={settings?.min}
|
min={settings?.min}
|
||||||
max={settings?.max}
|
max={settings?.max}
|
||||||
type="number"
|
|
||||||
step={settings?.step}
|
step={settings?.step}
|
||||||
placeholder={settings?.placeholder}
|
placeholder={settings?.placeholder}
|
||||||
onBlur={onValueChange}
|
onChange={onValueChange}
|
||||||
onKeyDown={onValueChange}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,11 +4,10 @@ import React, { FC, useCallback } from 'react';
|
|||||||
import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { InlineField, InlineFieldRow, RadioButtonGroup, Select, useStyles2 } from '@grafana/ui';
|
import { InlineField, InlineFieldRow, RadioButtonGroup, Select, useStyles2 } from '@grafana/ui';
|
||||||
import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/src/components/MatchersUI/utils';
|
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 { ScalarDimensionConfig, ScalarDimensionMode, ScalarDimensionOptions } from '../types';
|
||||||
|
|
||||||
import { NumberInput } from './NumberInput';
|
|
||||||
|
|
||||||
const fixedValueOption: SelectableValue<string> = {
|
const fixedValueOption: SelectableValue<string> = {
|
||||||
label: 'Fixed value',
|
label: 'Fixed value',
|
||||||
value: '_____fixed_____',
|
value: '_____fixed_____',
|
||||||
|
@ -3,6 +3,7 @@ import React, { FC, useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
|
import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
|
||||||
|
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useFieldDisplayNames,
|
useFieldDisplayNames,
|
||||||
@ -11,8 +12,6 @@ import {
|
|||||||
import { validateScaleOptions, validateScaleConfig } from '../scale';
|
import { validateScaleOptions, validateScaleConfig } from '../scale';
|
||||||
import { ScaleDimensionConfig, ScaleDimensionOptions } from '../types';
|
import { ScaleDimensionConfig, ScaleDimensionOptions } from '../types';
|
||||||
|
|
||||||
import { NumberInput } from './NumberInput';
|
|
||||||
|
|
||||||
const fixedValueOption: SelectableValue<string> = {
|
const fixedValueOption: SelectableValue<string> = {
|
||||||
label: 'Fixed value',
|
label: 'Fixed value',
|
||||||
value: '_____fixed_____',
|
value: '_____fixed_____',
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { FormEvent, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { DataTransformerID, standardTransformers, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
import { DataTransformerID, standardTransformers, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
HistogramTransformerOptions,
|
HistogramTransformerOptions,
|
||||||
histogramFieldInfo,
|
histogramFieldInfo,
|
||||||
} from '@grafana/data/src/transformations/transformers/histogram';
|
} 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>> = ({
|
export const HistogramTransformerEditor: React.FC<TransformerUIProps<HistogramTransformerOptions>> = ({
|
||||||
input,
|
input,
|
||||||
@ -15,22 +16,20 @@ export const HistogramTransformerEditor: React.FC<TransformerUIProps<HistogramTr
|
|||||||
const labelWidth = 18;
|
const labelWidth = 18;
|
||||||
|
|
||||||
const onBucketSizeChanged = useCallback(
|
const onBucketSizeChanged = useCallback(
|
||||||
(evt: FormEvent<HTMLInputElement>) => {
|
(val?: number) => {
|
||||||
const val = evt.currentTarget.valueAsNumber;
|
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
bucketSize: isNaN(val) ? undefined : val,
|
bucketSize: val,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onChange, options]
|
[onChange, options]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onBucketOffsetChanged = useCallback(
|
const onBucketOffsetChanged = useCallback(
|
||||||
(evt: FormEvent<HTMLInputElement>) => {
|
(val?: number) => {
|
||||||
const val = evt.currentTarget.valueAsNumber;
|
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
bucketOffset: isNaN(val) ? undefined : val,
|
bucketOffset: val,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onChange, options]
|
[onChange, options]
|
||||||
@ -51,7 +50,7 @@ export const HistogramTransformerEditor: React.FC<TransformerUIProps<HistogramTr
|
|||||||
label={histogramFieldInfo.bucketSize.name}
|
label={histogramFieldInfo.bucketSize.name}
|
||||||
tooltip={histogramFieldInfo.bucketSize.description}
|
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>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
@ -60,13 +59,7 @@ export const HistogramTransformerEditor: React.FC<TransformerUIProps<HistogramTr
|
|||||||
label={histogramFieldInfo.bucketOffset.name}
|
label={histogramFieldInfo.bucketOffset.name}
|
||||||
tooltip={histogramFieldInfo.bucketOffset.description}
|
tooltip={histogramFieldInfo.bucketOffset.description}
|
||||||
>
|
>
|
||||||
<Input
|
<NumberInput value={options.bucketOffset} placeholder="none" onChange={onBucketOffsetChanged} min={0} />
|
||||||
type="number"
|
|
||||||
value={options.bucketOffset}
|
|
||||||
placeholder="none"
|
|
||||||
onChange={onBucketOffsetChanged}
|
|
||||||
min={0}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
|
@ -4,8 +4,8 @@ import { Subject } from 'rxjs';
|
|||||||
|
|
||||||
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { Field, HorizontalGroup, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
|
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 { HorizontalConstraint, Placement, VerticalConstraint } from 'app/features/canvas';
|
||||||
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
|
||||||
|
|
||||||
import { PanelOptions } from '../models.gen';
|
import { PanelOptions } from '../models.gen';
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import React, { FC, useMemo, useCallback } from 'react';
|
|||||||
|
|
||||||
import { StandardEditorProps, SelectableValue } from '@grafana/data';
|
import { StandardEditorProps, SelectableValue } from '@grafana/data';
|
||||||
import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
|
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 { GeomapInstanceState } from '../GeomapPanel';
|
||||||
import { GeomapPanelOptions, MapViewConfig } from '../types';
|
import { GeomapPanelOptions, MapViewConfig } from '../types';
|
||||||
|
@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
|
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 { StyleEditor } from '../layers/data/StyleEditor';
|
||||||
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
|
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
|
||||||
|
Loading…
Reference in New Issue
Block a user