[Lens] Show validation feedback on top values out of bounds number of values (#110222)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2021-08-31 15:23:18 +02:00 committed by GitHub
parent c568a433f3
commit 3b81205a23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 123 additions and 33 deletions

View file

@ -348,13 +348,6 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
}); });
return ( return (
<> <>
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.terms.size', {
defaultMessage: 'Number of values',
})}
display="columnCompressed"
fullWidth
>
<ValuesInput <ValuesInput
value={currentColumn.params.size} value={currentColumn.params.size}
onChange={(value) => { onChange={(value) => {
@ -368,7 +361,6 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
); );
}} }}
/> />
</EuiFormRow>
<EuiFormRow <EuiFormRow
label={ label={
<> <>

View file

@ -8,7 +8,7 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { EuiFieldNumber } from '@elastic/eui'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { ValuesInput } from './values_input'; import { ValuesInput } from './values_input';
jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn()); jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn());
@ -41,7 +41,7 @@ describe('Values', () => {
expect(onChangeSpy.mock.calls[0][0]).toBe(7); expect(onChangeSpy.mock.calls[0][0]).toBe(7);
}); });
it('should not run onChange function on update when value is out of 1-100 range', () => { it('should not run onChange function on update when value is out of 1-1000 range', () => {
const onChangeSpy = jest.fn(); const onChangeSpy = jest.fn();
const instance = shallow(<ValuesInput value={5} onChange={onChangeSpy} />); const instance = shallow(<ValuesInput value={5} onChange={onChangeSpy} />);
act(() => { act(() => {
@ -54,4 +54,56 @@ describe('Values', () => {
expect(onChangeSpy.mock.calls.length).toBe(1); expect(onChangeSpy.mock.calls.length).toBe(1);
expect(onChangeSpy.mock.calls[0][0]).toBe(1000); expect(onChangeSpy.mock.calls[0][0]).toBe(1000);
}); });
it('should show an error message when the value is out of bounds', () => {
const instance = shallow(<ValuesInput value={-5} onChange={jest.fn()} />);
expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeTruthy();
expect(instance.find(EuiFormRow).prop('error')).toEqual(
expect.arrayContaining([expect.stringMatching('Value is lower')])
);
act(() => {
instance.find(EuiFieldNumber).prop('onChange')!({
currentTarget: { value: '1007' },
} as React.ChangeEvent<HTMLInputElement>);
});
instance.update();
expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeTruthy();
expect(instance.find(EuiFormRow).prop('error')).toEqual(
expect.arrayContaining([expect.stringMatching('Value is higher')])
);
});
it('should fallback to last valid value on input blur', () => {
const instance = shallow(<ValuesInput value={123} onChange={jest.fn()} />);
function changeAndBlur(newValue: string) {
act(() => {
instance.find(EuiFieldNumber).prop('onChange')!({
currentTarget: { value: newValue },
} as React.ChangeEvent<HTMLInputElement>);
});
instance.update();
act(() => {
instance.find(EuiFieldNumber).prop('onBlur')!({} as React.FocusEvent<HTMLInputElement>);
});
instance.update();
}
changeAndBlur('-5');
expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeFalsy();
expect(instance.find(EuiFieldNumber).prop('value')).toBe('1');
changeAndBlur('5000');
expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeFalsy();
expect(instance.find(EuiFieldNumber).prop('value')).toBe('1000');
changeAndBlur('');
// as we're not handling the onChange state, it fallbacks to the value prop
expect(instance.find(EuiFieldNumber).prop('value')).toBe('123');
});
}); });

View file

@ -7,7 +7,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiFieldNumber } from '@elastic/eui'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { useDebounceWithOptions } from '../../../../shared_components'; import { useDebounceWithOptions } from '../../../../shared_components';
export const ValuesInput = ({ export const ValuesInput = ({
@ -35,17 +35,63 @@ export const ValuesInput = ({
[inputValue] [inputValue]
); );
const isEmptyString = inputValue === '';
const isHigherThanMax = !isEmptyString && Number(inputValue) > MAX_NUMBER_OF_VALUES;
const isLowerThanMin = !isEmptyString && Number(inputValue) < MIN_NUMBER_OF_VALUES;
return ( return (
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.terms.size', {
defaultMessage: 'Number of values',
})}
display="columnCompressed"
fullWidth
isInvalid={isHigherThanMax || isLowerThanMin}
error={
isHigherThanMax
? [
i18n.translate('xpack.lens.indexPattern.terms.sizeLimitMax', {
defaultMessage:
'Value is higher than the maximum {max}, the maximum value is used instead.',
values: {
max: MAX_NUMBER_OF_VALUES,
},
}),
]
: isLowerThanMin
? [
i18n.translate('xpack.lens.indexPattern.terms.sizeLimitMin', {
defaultMessage:
'Value is lower than the minimum {min}, the minimum value is used instead.',
values: {
min: MIN_NUMBER_OF_VALUES,
},
}),
]
: null
}
>
<EuiFieldNumber <EuiFieldNumber
min={MIN_NUMBER_OF_VALUES} min={MIN_NUMBER_OF_VALUES}
max={MAX_NUMBER_OF_VALUES} max={MAX_NUMBER_OF_VALUES}
step={1} step={1}
value={inputValue} value={inputValue}
compressed compressed
isInvalid={isHigherThanMax || isLowerThanMin}
onChange={({ currentTarget }) => setInputValue(currentTarget.value)} onChange={({ currentTarget }) => setInputValue(currentTarget.value)}
aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', {
defaultMessage: 'Number of values', defaultMessage: 'Number of values',
})} })}
onBlur={() => {
if (inputValue === '') {
return setInputValue(String(value));
}
const inputNumber = Number(inputValue);
setInputValue(
String(Math.min(MAX_NUMBER_OF_VALUES, Math.max(inputNumber, MIN_NUMBER_OF_VALUES)))
);
}}
/> />
</EuiFormRow>
); );
}; };