mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
8673fb6c59
commit
45a5ad55bc
4 changed files with 240 additions and 31 deletions
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue