[Default editor] Catch invalid calendar exception (#60494)

* Catch invalid calendar exception

* Use isValidEsInterval directly

* Show field error message right away

* Fix for the case 2w

* Update time_interval.tsx

* Restructure validation

* Rename fn to isValidCalendarInterval

* Refactoring

* Update time_interval.tsx

* Add functional tests

* Add functional tests for interval

* Update _area_chart.js

* Don't show error when value is empty

* Use error message from InvalidEsCalendarIntervalError

* Update _area_chart.js

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Maryia Lapata 2020-03-26 18:24:03 +03:00 committed by GitHub
parent 8673fb6c59
commit 45a5ad55bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 240 additions and 31 deletions

View file

@ -80,6 +80,8 @@ function FieldParamEditor({
}
const isValid = !!value && !errors.length && !isDirty;
// we show an error message right away if there is no compatible fields
const showErrorMessage = (showValidation || !indexedFields.length) && !isValid;
useValidation(setValidity, isValid);
@ -103,7 +105,7 @@ function FieldParamEditor({
return (
<EuiFormRow
label={customLabel || label}
isInvalid={showValidation ? !isValid : false}
isInvalid={showErrorMessage}
fullWidth={true}
error={errors}
compressed
@ -118,7 +120,7 @@ function FieldParamEditor({
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
isClearable={false}
isInvalid={showValidation ? !isValid : false}
isInvalid={showErrorMessage}
onChange={onChange}
onBlur={setTouched}
onSearchChange={onSearchChange}

View file

@ -25,6 +25,73 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { search, AggParamOption } from '../../../../../../plugins/data/public';
import { AggParamEditorProps } from '../agg_param_props';
const { parseEsInterval, InvalidEsCalendarIntervalError } = search.aggs;
// we check if Elasticsearch interval is valid to show a user appropriate error message
// e.g. there is the case when a user inputs '14d' but it's '2w' in expression equivalent and the request will fail
// we don't check it for 0ms because the overall time range has not yet been set
function isValidCalendarInterval(interval: string) {
if (interval === '0ms') {
return { isValidCalendarValue: true };
}
try {
parseEsInterval(interval);
return { isValidCalendarValue: true };
} catch (e) {
if (e instanceof InvalidEsCalendarIntervalError) {
return { isValidCalendarValue: false, error: e.message };
}
return { isValidCalendarValue: true };
}
}
const errorMessage = i18n.translate(
'visDefaultEditor.controls.timeInterval.invalidFormatErrorMessage',
{
defaultMessage: 'Invalid interval format.',
}
);
function validateInterval(
agg: any,
value?: string,
definedOption?: ComboBoxOption,
timeBase?: string
) {
if (definedOption) {
return { isValid: true };
}
if (!value) {
return { isValid: false };
}
if (!timeBase) {
// we check if Elasticsearch interval is valid ES interval to show a user appropriate error message
// we don't check if there is timeBase
const { isValidCalendarValue, error } = isValidCalendarInterval(value);
if (!isValidCalendarValue) {
return { isValid: false, error };
}
}
const isValid = search.aggs.isValidInterval(value, timeBase);
if (!isValid) {
return { isValid: false, error: errorMessage };
}
const interval = agg.buckets?.getInterval();
const { isValidCalendarValue, error } = isValidCalendarInterval(interval.expression);
if (!isValidCalendarValue) {
return { isValid: false, error };
}
return { isValid, interval };
}
interface ComboBoxOption extends EuiComboBoxOptionOption {
key: string;
@ -55,16 +122,15 @@ function TimeIntervalParamEditor({
let selectedOptions: ComboBoxOption[] = [];
let definedOption: ComboBoxOption | undefined;
let isValid = false;
if (value) {
definedOption = find(options, { key: value });
selectedOptions = definedOption ? [definedOption] : [{ label: value, key: 'custom' }];
isValid = !!(definedOption || search.aggs.isValidInterval(value, timeBase));
}
const interval = get(agg, 'buckets.getInterval') && (agg as any).buckets.getInterval();
const { isValid, error, interval } = validateInterval(agg, value, definedOption, timeBase);
const scaledHelpText =
interval && interval.scaled && isValid ? (
interval && interval.scaled ? (
<strong className="eui-displayBlock">
<FormattedMessage
id="visDefaultEditor.controls.timeInterval.scaledHelpText"
@ -86,32 +152,11 @@ function TimeIntervalParamEditor({
</>
);
const errors = [];
if (!isValid && value) {
errors.push(
i18n.translate('visDefaultEditor.controls.timeInterval.invalidFormatErrorMessage', {
defaultMessage: 'Invalid interval format.',
})
);
}
const onCustomInterval = (customValue: string) => {
const normalizedCustomValue = customValue.trim();
setValue(normalizedCustomValue);
if (normalizedCustomValue && search.aggs.isValidInterval(normalizedCustomValue, timeBase)) {
agg.write();
}
};
const onCustomInterval = (customValue: string) => setValue(customValue.trim());
const onChange = (opts: EuiComboBoxOptionOption[]) => {
const selectedOpt: ComboBoxOption = get(opts, '0');
setValue(selectedOpt ? selectedOpt.key : '');
if (selectedOpt) {
agg.write();
}
};
useEffect(() => {
@ -121,10 +166,10 @@ function TimeIntervalParamEditor({
return (
<EuiFormRow
compressed
error={errors}
error={error}
fullWidth={true}
helpText={helpText}
isInvalid={showValidation ? !isValid : false}
isInvalid={showValidation && !isValid}
label={i18n.translate('visDefaultEditor.controls.timeInterval.minimumIntervalLabel', {
defaultMessage: 'Minimum interval',
})}
@ -133,7 +178,7 @@ function TimeIntervalParamEditor({
compressed
fullWidth={true}
data-test-subj="visEditorInterval"
isInvalid={showValidation ? !isValid : false}
isInvalid={showValidation && !isValid}
noSuggestions={!!timeBase}
onChange={onChange}
onCreateOption={onCustomInterval}

View file

@ -21,10 +21,12 @@ import expect from '@kbn/expect';
export default function({ getService, getPageObjects }) {
const log = getService('log');
const find = getService('find');
const inspector = getService('inspector');
const browser = getService('browser');
const retry = getService('retry');
const security = getService('security');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects([
'common',
'visualize',
@ -465,5 +467,92 @@ export default function({ getService, getPageObjects }) {
expect(paths.length).to.eql(numberOfSegments);
});
});
describe('date histogram when no date field', () => {
before(async () => {
await PageObjects.visualize.loadSavedVisualization('AreaChart [no date field]');
await PageObjects.visChart.waitForVisualization();
log.debug('Click X-axis');
await PageObjects.visEditor.clickBucket('X-axis');
log.debug('Click Date Histogram');
await PageObjects.visEditor.selectAggregation('Date Histogram');
});
it('should show error message for field', async () => {
const fieldErrorMessage = await find.byCssSelector(
'[data-test-subj="visDefaultEditorField"] + .euiFormErrorText'
);
const errorMessage = await fieldErrorMessage.getVisibleText();
expect(errorMessage).to.be(
'The index pattern test_index* does not contain any of the following compatible field types: date'
);
});
});
describe('date histogram when no time filter', () => {
before(async () => {
await PageObjects.visualize.loadSavedVisualization('AreaChart [no time filter]');
await PageObjects.visChart.waitForVisualization();
log.debug('Click X-axis');
await PageObjects.visEditor.clickBucket('X-axis');
log.debug('Click Date Histogram');
await PageObjects.visEditor.selectAggregation('Date Histogram');
});
it('should not show error message on init when the field is not selected', async () => {
const fieldValues = await PageObjects.visEditor.getField();
expect(fieldValues[0]).to.be(undefined);
const isFieldErrorMessageExists = await find.existsByCssSelector(
'[data-test-subj="visDefaultEditorField"] + .euiFormErrorText'
);
expect(isFieldErrorMessageExists).to.be(false);
});
describe('interval errors', () => {
before(async () => {
// to trigger displaying of error messages
await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
// this will avoid issues with the play tooltip covering the interval field
await testSubjects.scrollIntoView('advancedParams-2');
});
it('should not fail during changing interval when the field is not selected', async () => {
await PageObjects.visEditor.setInterval('m');
const intervalValues = await PageObjects.visEditor.getInterval();
expect(intervalValues[0]).to.be('Millisecond');
});
it('should not fail during changing custom interval when the field is not selected', async () => {
await PageObjects.visEditor.setInterval('4d', { type: 'custom' });
const isInvalidIntervalExists = await find.existsByCssSelector(
'.euiComboBox-isInvalid[data-test-subj="visEditorInterval"]'
);
expect(isInvalidIntervalExists).to.be(false);
});
it('should show error when interval invalid', async () => {
await PageObjects.visEditor.setInterval('xx', { type: 'custom' });
const isIntervalErrorMessageExists = await find.existsByCssSelector(
'[data-test-subj="visEditorInterval"] + .euiFormErrorText'
);
expect(isIntervalErrorMessageExists).to.be(true);
});
it('should show error when calendar interval invalid', async () => {
await PageObjects.visEditor.setInterval('14d', { type: 'custom' });
const intervalErrorMessage = await find.byCssSelector(
'[data-test-subj="visEditorInterval"] + .euiFormErrorText'
);
let errorMessage = await intervalErrorMessage.getVisibleText();
expect(errorMessage).to.be('Invalid calendar interval: 2w, value must be 1');
await PageObjects.visEditor.setInterval('3w', { type: 'custom' });
errorMessage = await intervalErrorMessage.getVisibleText();
expect(errorMessage).to.be('Invalid calendar interval: 3w, value must be 1');
});
});
});
});
}

File diff suppressed because one or more lines are too long