[ML] Data Frames: Fix form validation (#40427)
Fixes the form validation of transform id and destination index name. - In addition to checking against existing transform ids, the input is now validated according to the data frame API docs - The destination index input field is now validated against the index name limitations found in Elasticsearch's documentation. Since these rules are too much to be added as a helper text, a link to the docs is part of the helper text. - If the destination index name matches an existing index, the Next button is no longer disabled, but the helper text for the input field states that creating this transform will modify the existing destination index.
This commit is contained in:
parent
3c7702227d
commit
f32a20ca20
5 changed files with 164 additions and 21 deletions
57
x-pack/legacy/plugins/ml/common/util/es_utils.test.ts
Normal file
57
x-pack/legacy/plugins/ml/common/util/es_utils.test.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isValidIndexName } from './es_utils';
|
||||
|
||||
describe('Util: isValidIndexName()', () => {
|
||||
test('Validation checks.', () => {
|
||||
// Lowercase only
|
||||
expect(isValidIndexName('lorem')).toBe(true);
|
||||
expect(isValidIndexName('loRem')).toBe(false);
|
||||
|
||||
// Cannot include \, /, *, ?, ", <, >, |, space character, comma, #, :
|
||||
expect(isValidIndexName('\\')).toBe(false);
|
||||
expect(isValidIndexName('/')).toBe(false);
|
||||
expect(isValidIndexName('*')).toBe(false);
|
||||
expect(isValidIndexName('?')).toBe(false);
|
||||
expect(isValidIndexName('"')).toBe(false);
|
||||
expect(isValidIndexName('<')).toBe(false);
|
||||
expect(isValidIndexName('>')).toBe(false);
|
||||
expect(isValidIndexName('|')).toBe(false);
|
||||
expect(isValidIndexName(' ')).toBe(false);
|
||||
expect(isValidIndexName(',')).toBe(false);
|
||||
expect(isValidIndexName('#')).toBe(false);
|
||||
|
||||
// Cannot start with -, _, +
|
||||
expect(isValidIndexName('lorem-ipsum')).toBe(true);
|
||||
expect(isValidIndexName('lorem_ipsum')).toBe(true);
|
||||
expect(isValidIndexName('lorem+ipsum')).toBe(true);
|
||||
expect(isValidIndexName('lorem-')).toBe(true);
|
||||
expect(isValidIndexName('lorem_')).toBe(true);
|
||||
expect(isValidIndexName('lorem+')).toBe(true);
|
||||
expect(isValidIndexName('-lorem')).toBe(false);
|
||||
expect(isValidIndexName('_lorem')).toBe(false);
|
||||
expect(isValidIndexName('+lorem')).toBe(false);
|
||||
|
||||
// Cannot be . or ..
|
||||
expect(isValidIndexName('lorem.ipsum')).toBe(true);
|
||||
expect(isValidIndexName('lorem.')).toBe(true);
|
||||
expect(isValidIndexName('.lorem')).toBe(true);
|
||||
expect(isValidIndexName('lorem..ipsum')).toBe(true);
|
||||
expect(isValidIndexName('lorem..')).toBe(true);
|
||||
expect(isValidIndexName('..lorem')).toBe(true);
|
||||
expect(isValidIndexName('.')).toBe(false);
|
||||
expect(isValidIndexName('..')).toBe(false);
|
||||
|
||||
// Cannot be longer than 255 bytes (note it is bytes,
|
||||
// so multi-byte characters will count towards the 255 limit faster)
|
||||
expect(isValidIndexName('a'.repeat(255))).toBe(true);
|
||||
expect(isValidIndexName('a'.repeat(256))).toBe(false);
|
||||
// multi-byte character test
|
||||
// because jest doesn't have TextEncoder this will still be true
|
||||
expect(isValidIndexName('あ'.repeat(255))).toBe(true);
|
||||
});
|
||||
});
|
41
x-pack/legacy/plugins/ml/common/util/es_utils.ts
Normal file
41
x-pack/legacy/plugins/ml/common/util/es_utils.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
interface WindowWithTextEncoder extends Window {
|
||||
TextEncoder: any;
|
||||
}
|
||||
|
||||
const windowWithTextEncoder = window as WindowWithTextEncoder;
|
||||
|
||||
function isValidIndexNameLength(indexName: string) {
|
||||
if (
|
||||
windowWithTextEncoder.TextEncoder &&
|
||||
new windowWithTextEncoder.TextEncoder('utf-8').encode(indexName).length > 255
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If TextEncoder is not available just check for string.length
|
||||
return indexName.length <= 255;
|
||||
}
|
||||
|
||||
// rules taken from
|
||||
// https://github.com/elastic/elasticsearch/blob/master/docs/reference/indices/create-index.asciidoc
|
||||
export function isValidIndexName(indexName: string) {
|
||||
return (
|
||||
// Lowercase only
|
||||
indexName === indexName.toLowerCase() &&
|
||||
// Cannot include \, /, *, ?, ", <, >, |, space character, comma, #, :
|
||||
/^[^\*\\/\?"<>|\s,#:]+$/.test(indexName) &&
|
||||
// Cannot start with -, _, +
|
||||
/^[^-_\+]+$/.test(indexName.charAt(0)) &&
|
||||
// Cannot be . or ..
|
||||
(indexName !== '.' && indexName !== '..') &&
|
||||
// Cannot be longer than 255 bytes (note it is bytes,
|
||||
// so multi-byte characters will count towards the 255 limit faster)
|
||||
isValidIndexNameLength(indexName)
|
||||
);
|
||||
}
|
|
@ -7,9 +7,14 @@
|
|||
import React, { Fragment, SFC, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { metadata } from 'ui/metadata';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import { EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
|
||||
// @ts-ignore
|
||||
import { isJobIdValid } from '../../../../common/util/job_utils';
|
||||
import { isValidIndexName } from '../../../../common/util/es_utils';
|
||||
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
|
@ -124,13 +129,20 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
|
|||
}, []);
|
||||
|
||||
const jobIdExists = jobIds.some(id => jobId === id);
|
||||
const jobIdEmpty = jobId === '';
|
||||
const jobIdValid = isJobIdValid(jobId);
|
||||
|
||||
const indexNameExists = indexNames.some(name => destinationIndex === name);
|
||||
const indexNameEmpty = destinationIndex === '';
|
||||
const indexNameValid = isValidIndexName(destinationIndex);
|
||||
const indexPatternTitleExists = indexPatternTitles.some(name => destinationIndex === name);
|
||||
|
||||
const valid =
|
||||
jobId !== '' &&
|
||||
destinationIndex !== '' &&
|
||||
!jobIdEmpty &&
|
||||
jobIdValid &&
|
||||
!jobIdExists &&
|
||||
!indexNameExists &&
|
||||
!indexNameEmpty &&
|
||||
indexNameValid &&
|
||||
(!indexPatternTitleExists || !createIndexPattern) &&
|
||||
(!isContinuousModeAvailable || (isContinuousModeAvailable && isContinuousModeDelayValid));
|
||||
|
||||
|
@ -164,14 +176,24 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
|
|||
label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdLabel', {
|
||||
defaultMessage: 'Transform id',
|
||||
})}
|
||||
isInvalid={jobIdExists}
|
||||
error={
|
||||
jobIdExists && [
|
||||
i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdError', {
|
||||
defaultMessage: 'A transform with this id already exists.',
|
||||
}),
|
||||
]
|
||||
}
|
||||
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
|
||||
error={[
|
||||
...(!jobIdEmpty && !jobIdValid
|
||||
? [
|
||||
i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdInvalidError', {
|
||||
defaultMessage:
|
||||
'Must contain lowercase alphanumeric characters (a-z and 0-9), hyphens, and underscores only and must start and end with alphanumeric characters.',
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(jobIdExists
|
||||
? [
|
||||
i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdExistsError', {
|
||||
defaultMessage: 'A transform with this id already exists.',
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder="transform id"
|
||||
|
@ -180,13 +202,16 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
|
|||
aria-label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdInputAriaLabel', {
|
||||
defaultMessage: 'Choose a unique transform id.',
|
||||
})}
|
||||
isInvalid={jobIdExists}
|
||||
isInvalid={(!jobIdEmpty && !jobIdValid) || jobIdExists}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobDescriptionLabel', {
|
||||
defaultMessage: 'Transform description',
|
||||
})}
|
||||
helpText={i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobDescriptionHelpText', {
|
||||
defaultMessage: 'Optional descriptive text.',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder="transform description"
|
||||
|
@ -204,12 +229,34 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
|
|||
label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.destinationIndexLabel', {
|
||||
defaultMessage: 'Destination index',
|
||||
})}
|
||||
isInvalid={indexNameExists}
|
||||
isInvalid={!indexNameEmpty && !indexNameValid}
|
||||
helpText={
|
||||
indexNameExists &&
|
||||
i18n.translate('xpack.ml.dataframe.jobDetailsForm.destinationIndexHelpText', {
|
||||
defaultMessage:
|
||||
'An index with this name already exists. Be aware that running this transform will modify this destination index.',
|
||||
})
|
||||
}
|
||||
error={
|
||||
indexNameExists && [
|
||||
i18n.translate('xpack.ml.dataframe.jobDetailsForm.destinationIndexError', {
|
||||
defaultMessage: 'An index with this name already exists.',
|
||||
}),
|
||||
!indexNameEmpty &&
|
||||
!indexNameValid && [
|
||||
<Fragment>
|
||||
{i18n.translate('xpack.ml.dataframe.jobDetailsForm.destinationIndexInvalidError', {
|
||||
defaultMessage: 'Invalid destination index name.',
|
||||
})}
|
||||
<br />
|
||||
<EuiLink
|
||||
href={`https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/indices-create-index.html#indices-create-index`}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.definePivotForm.destinationIndexInvalidErrorLink',
|
||||
{
|
||||
defaultMessage: 'Learn more about index name limitations.',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</Fragment>,
|
||||
]
|
||||
}
|
||||
>
|
||||
|
@ -223,7 +270,7 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
|
|||
defaultMessage: 'Choose a unique destination index name.',
|
||||
}
|
||||
)}
|
||||
isInvalid={indexNameExists}
|
||||
isInvalid={!indexNameEmpty && !indexNameValid}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
|
|
|
@ -6219,7 +6219,6 @@
|
|||
"xpack.ml.dataframe.jobDetailsForm.errorGettingDataFrameJobsList": "既存のデータフレームジョブIDの取得中にエラーが発生しました:{error}",
|
||||
"xpack.ml.dataframe.jobDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました: {error}",
|
||||
"xpack.ml.dataframe.jobDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。",
|
||||
"xpack.ml.dataframe.jobDetailsForm.jobIdError": "この ID のジョブが既に存在します。",
|
||||
"xpack.ml.dataframe.jobDetailsForm.jobIdInputAriaLabel": "固有のジョブ ID を選択してください。",
|
||||
"xpack.ml.dataframe.jobDetailsForm.jobIdLabel": "ジョブ ID",
|
||||
"xpack.ml.dataframe.jobDetailsSummary.createIndexPatternMessage": "このジョブの Kibana インデックスパターンが作成されます。",
|
||||
|
|
|
@ -6219,7 +6219,6 @@
|
|||
"xpack.ml.dataframe.jobDetailsForm.errorGettingDataFrameJobsList": "获取现有数据帧作业 ID 时发生错误:{error}",
|
||||
"xpack.ml.dataframe.jobDetailsForm.errorGettingIndexPatternTitles": "获取现有索引名称时发生错误:{error}",
|
||||
"xpack.ml.dataframe.jobDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。",
|
||||
"xpack.ml.dataframe.jobDetailsForm.jobIdError": "已存在具有此 ID 的作业。",
|
||||
"xpack.ml.dataframe.jobDetailsForm.jobIdInputAriaLabel": "选择唯一的作业 ID。",
|
||||
"xpack.ml.dataframe.jobDetailsForm.jobIdLabel": "作业 ID",
|
||||
"xpack.ml.dataframe.jobDetailsSummary.createIndexPatternMessage": "将为此作业创建 Kibana 索引模式。",
|
||||
|
|
Loading…
Reference in a new issue