mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
This commit is contained in:
parent
7c5e01fb94
commit
8651c64a29
85 changed files with 2506 additions and 902 deletions
53
src/plugins/es_ui_shared/static/forms/components/field.tsx
Normal file
53
src/plugins/es_ui_shared/static/forms/components/field.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FieldHook, FIELD_TYPES } from '../hook_form_lib';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
import {
|
||||
TextField,
|
||||
NumericField,
|
||||
CheckBoxField,
|
||||
ComboBoxField,
|
||||
MultiSelectField,
|
||||
SelectField,
|
||||
ToggleField,
|
||||
} from './fields';
|
||||
|
||||
const mapTypeToFieldComponent = {
|
||||
[FIELD_TYPES.TEXT]: TextField,
|
||||
[FIELD_TYPES.NUMBER]: NumericField,
|
||||
[FIELD_TYPES.CHECKBOX]: CheckBoxField,
|
||||
[FIELD_TYPES.COMBO_BOX]: ComboBoxField,
|
||||
[FIELD_TYPES.MULTI_SELECT]: MultiSelectField,
|
||||
[FIELD_TYPES.SELECT]: SelectField,
|
||||
[FIELD_TYPES.TOGGLE]: ToggleField,
|
||||
};
|
||||
|
||||
export const Field = (props: Props) => {
|
||||
const FieldComponent = mapTypeToFieldComponent[props.field.type] || TextField;
|
||||
return <FieldComponent {...props} />;
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiCheckbox } from '@elastic/eui';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { FieldHook } from '../../hook_form_lib';
|
||||
import { getFieldValidityAndErrorMessage } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiCheckbox
|
||||
label={field.label}
|
||||
checked={field.value as boolean}
|
||||
onChange={field.onChange}
|
||||
id={euiFieldProps.id || uuid()}
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
|
||||
|
||||
import { FieldHook, VALIDATION_TYPES, FieldValidateResponse } from '../../hook_form_lib';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
// Errors for the comboBox value (the "array")
|
||||
const errorMessageField = field.getErrorsMessages();
|
||||
|
||||
// Errors for comboBox option added (the array "item")
|
||||
const errorMessageArrayItem = field.getErrorsMessages({
|
||||
validationType: VALIDATION_TYPES.ARRAY_ITEM,
|
||||
});
|
||||
|
||||
const isInvalid = field.errors.length
|
||||
? errorMessageField !== null || errorMessageArrayItem !== null
|
||||
: false;
|
||||
|
||||
// Concatenate error messages.
|
||||
const errorMessage =
|
||||
errorMessageField && errorMessageArrayItem
|
||||
? `${errorMessageField}, ${errorMessageArrayItem}`
|
||||
: errorMessageField
|
||||
? errorMessageField
|
||||
: errorMessageArrayItem;
|
||||
|
||||
const onCreateComboOption = (value: string) => {
|
||||
// Note: for now, all validations for a comboBox array item have to be synchronous
|
||||
// If there is a need to support asynchronous validation, we'll work on it (and will need to update the <EuiComboBox /> logic).
|
||||
const { isValid } = field.validate({
|
||||
value,
|
||||
validationType: VALIDATION_TYPES.ARRAY_ITEM,
|
||||
}) as FieldValidateResponse;
|
||||
|
||||
if (!isValid) {
|
||||
// Return false to explicitly reject the user's input.
|
||||
return false;
|
||||
}
|
||||
|
||||
const newValue = [...(field.value as string[]), value];
|
||||
|
||||
field.setValue(newValue);
|
||||
};
|
||||
|
||||
const onComboChange = (options: EuiComboBoxOptionProps[]) => {
|
||||
field.setValue(options.map(option => option.label));
|
||||
};
|
||||
|
||||
const onSearchComboChange = (value: string) => {
|
||||
if (value) {
|
||||
field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
placeholder={i18n.translate('esUi.forms.comboBoxField.placeHolderText', {
|
||||
defaultMessage: 'Type and then hit "ENTER"',
|
||||
})}
|
||||
selectedOptions={(field.value as any[]).map(v => ({ label: v }))}
|
||||
onCreateOption={onCreateComboOption}
|
||||
onChange={onComboChange}
|
||||
onSearchChange={onSearchComboChange}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './text_field';
|
||||
export * from './numeric_field';
|
||||
export * from './checkbox_field';
|
||||
export * from './combobox_field';
|
||||
export * from './multi_select_field';
|
||||
export * from './select_field';
|
||||
export * from './toggle_field';
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiSelectable, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { FieldHook } from '../../hook_form_lib';
|
||||
import { getFieldValidityAndErrorMessage } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const MultiSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiSelectable
|
||||
allowExclusions={false}
|
||||
height={300}
|
||||
onChange={options => {
|
||||
field.setValue(options);
|
||||
}}
|
||||
options={field.value as any[]}
|
||||
data-test-subj="select"
|
||||
{...euiFieldProps}
|
||||
>
|
||||
{(list, search) => (
|
||||
<EuiPanel paddingSize="s" hasShadow={false}>
|
||||
{search}
|
||||
{list}
|
||||
</EuiPanel>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiFieldNumber } from '@elastic/eui';
|
||||
|
||||
import { FieldHook } from '../../hook_form_lib';
|
||||
import { getFieldValidityAndErrorMessage } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const NumericField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
isInvalid={isInvalid}
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
isLoading={field.isValidating}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
|
||||
import { FieldHook } from '../../hook_form_lib';
|
||||
import { getFieldValidityAndErrorMessage } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
value={field.value as string}
|
||||
onChange={e => {
|
||||
field.setValue(e.target.value);
|
||||
}}
|
||||
hasNoInitialSelection={true}
|
||||
isInvalid={isInvalid}
|
||||
data-test-subj="select"
|
||||
{...(euiFieldProps as { options: any; [key: string]: any })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
|
||||
|
||||
import { FieldHook } from '../../hook_form_lib';
|
||||
import { getFieldValidityAndErrorMessage } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const TextField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiFieldText
|
||||
isInvalid={isInvalid}
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
isLoading={field.isValidating}
|
||||
fullWidth
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import { FieldHook } from '../../hook_form_lib';
|
||||
import { getFieldValidityAndErrorMessage } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
field: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const ToggleField = ({ field, euiFieldProps = {}, ...rest }: Props) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
helpText={field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
describedByIds={rest.idAria ? [rest.idAria] : undefined}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={field.label}
|
||||
checked={field.value as boolean}
|
||||
onChange={field.onChange}
|
||||
data-test-subj="input"
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiDescribedFormGroup, EuiTitle } from '@elastic/eui';
|
||||
import { FieldHook } from '../hook_form_lib';
|
||||
import { Field } from './field';
|
||||
|
||||
interface Props {
|
||||
title: string | JSX.Element;
|
||||
description?: string | JSX.Element;
|
||||
field?: FieldHook;
|
||||
euiFieldProps?: Record<string, any>;
|
||||
idAria?: string;
|
||||
titleTag?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
children?: React.ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const FormRow = ({
|
||||
title,
|
||||
idAria,
|
||||
description,
|
||||
field,
|
||||
children,
|
||||
titleTag = 'h4',
|
||||
...rest
|
||||
}: Props) => {
|
||||
let titleWrapped = title;
|
||||
|
||||
// If a string is provided, create a default Euititle of size "m"
|
||||
const isTitleString = typeof title === 'string' || title.type.name === 'FormattedMessage';
|
||||
if (isTitleString) {
|
||||
// Create the correct title tag
|
||||
const titleWithHTag = React.createElement(titleTag, undefined, title);
|
||||
titleWrapped = <EuiTitle size="s">{titleWithHTag}</EuiTitle>;
|
||||
}
|
||||
|
||||
if (!children && !field) {
|
||||
throw new Error('You need to provide either children or a field to the FormRow');
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup title={titleWrapped} description={description} idAria={idAria} fullWidth>
|
||||
{children ? children : <Field field={field!} idAria={idAria} {...rest} />}
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
30
src/plugins/es_ui_shared/static/forms/components/helpers.ts
Normal file
30
src/plugins/es_ui_shared/static/forms/components/helpers.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FieldHook } from '../hook_form_lib';
|
||||
|
||||
export const getFieldValidityAndErrorMessage = (
|
||||
field: FieldHook
|
||||
): { isInvalid: boolean; errorMessage: string | null } => {
|
||||
const isInvalid = !field.isChangingValue && field.errors.length > 0;
|
||||
const errorMessage =
|
||||
!field.isChangingValue && field.errors.length ? field.errors[0].message : null;
|
||||
|
||||
return { isInvalid, errorMessage };
|
||||
};
|
22
src/plugins/es_ui_shared/static/forms/components/index.ts
Normal file
22
src/plugins/es_ui_shared/static/forms/components/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './field';
|
||||
export * from './form_row';
|
||||
export * from './fields';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
import { SerializerFunc } from '../hook_form_lib';
|
||||
|
||||
type FuncType = (selectOptions: Option[]) => SerializerFunc;
|
||||
|
||||
export const multiSelectComponent: Record<string, FuncType> = {
|
||||
// This deSerializer takes the previously selected options and map them
|
||||
// against the default select options values.
|
||||
selectedValueToOptions(selectOptions) {
|
||||
return defaultFormValue => {
|
||||
// If there are no default form value, it means that no previous value has been selected.
|
||||
if (!defaultFormValue) {
|
||||
return selectOptions;
|
||||
}
|
||||
|
||||
return (selectOptions as Option[]).map(option => ({
|
||||
...option,
|
||||
checked: (defaultFormValue as string[]).includes(option.label) ? 'on' : undefined,
|
||||
}));
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* NOTE: The toInt() formatter does _not_ play well if we enter the "e" letter in a "number" input
|
||||
* as it does not trigger an "onChange" event.
|
||||
* I searched if it was a bug and found this thread (https://github.com/facebook/react/pull/7359#event-1017024857)
|
||||
* We will need to investigate this a little further.
|
||||
*
|
||||
* @param value The string value to convert to number
|
||||
*/
|
||||
export const toInt = (value: string): number => parseFloat(value);
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ValidationFunc, ValidationError } from '../../hook_form_lib';
|
||||
import { containsChars } from '../../../validators/string';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
export const containsCharsField = ({
|
||||
message,
|
||||
chars,
|
||||
}: {
|
||||
message: string | ((err: Partial<ValidationError>) => string);
|
||||
chars: string | string[];
|
||||
}) => (...args: Parameters<ValidationFunc>): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { doesContain, charsFound } = containsChars(chars)(value as string);
|
||||
if (doesContain) {
|
||||
return {
|
||||
code: 'ERR_INVALID_CHARS',
|
||||
charsFound,
|
||||
message: typeof message === 'function' ? message({ charsFound }) : message,
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ValidationFunc } from '../../hook_form_lib';
|
||||
import { isEmptyString } from '../../../validators/string';
|
||||
import { isEmptyArray } from '../../../validators/array';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
export const emptyField = (message: string) => (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value, path }] = args;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return isEmptyString(value) ? { code: 'ERR_FIELD_MISSING', path, message } : undefined;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return isEmptyArray(value) ? { code: 'ERR_FIELD_MISSING', path, message } : undefined;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './empty_field';
|
||||
export * from './min_length';
|
||||
export * from './min_selectable_selection';
|
||||
export * from './url';
|
||||
export * from './index_name';
|
||||
export * from './contains_char';
|
||||
export * from './starts_with';
|
||||
export * from './index_pattern_field';
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Note: we can't import from "ui/indices" as the TS Type definition don't exist
|
||||
// import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices';
|
||||
import { ValidationFunc } from '../../hook_form_lib';
|
||||
import { startsWith, containsChars } from '../../../validators/string';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
const INDEX_ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', '*'];
|
||||
|
||||
export const indexNameField = (i18n: any) => (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
if (startsWith('.')(value as string)) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
formatType: 'INDEX_NAME',
|
||||
message: i18n.translate('esUi.forms.fieldValidation.indexNameStartsWithDotError', {
|
||||
defaultMessage: 'The index name cannot start with a dot (.).',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const { doesContain: doesContainSpaces } = containsChars(' ')(value as string);
|
||||
if (doesContainSpaces) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
formatType: 'INDEX_NAME',
|
||||
message: i18n.translate('esUi.forms.fieldValidation.indexNameSpacesError', {
|
||||
defaultMessage: 'The index name cannot contain spaces.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const { charsFound, doesContain } = containsChars(INDEX_ILLEGAL_CHARACTERS)(value as string);
|
||||
if (doesContain) {
|
||||
return {
|
||||
message: i18n.translate('esUi.forms.fieldValidation.indexNameInvalidCharactersError', {
|
||||
defaultMessage:
|
||||
'The index name contains the invalid {characterListLength, plural, one {character} other {characters}} { characterList }.',
|
||||
values: {
|
||||
characterList: charsFound.join(' '),
|
||||
characterListLength: charsFound.length,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ILLEGAL_CHARACTERS, validateIndexPattern } from 'ui/index_patterns';
|
||||
|
||||
import { ValidationFunc } from '../../hook_form_lib';
|
||||
import { containsChars } from '../../../validators/string';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
export const indexPatternField = (i18n: any) => (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate it does not contain spaces
|
||||
const { doesContain } = containsChars(' ')(value);
|
||||
|
||||
if (doesContain) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
formatType: 'INDEX_PATTERN',
|
||||
message: i18n.translate('esUi.forms.fieldValidation.indexPatternSpacesError', {
|
||||
defaultMessage: 'The index pattern cannot contain spaces.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate illegal characters
|
||||
const errors = validateIndexPattern(value);
|
||||
|
||||
if (errors[ILLEGAL_CHARACTERS]) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
formatType: 'INDEX_PATTERN',
|
||||
message: i18n.translate('esUi.forms.fieldValidation.indexPatternInvalidCharactersError', {
|
||||
defaultMessage:
|
||||
'The index pattern contains the invalid {characterListLength, plural, one {character} other {characters}} { characterList }.',
|
||||
values: {
|
||||
characterList: errors[ILLEGAL_CHARACTERS].join(' '),
|
||||
characterListLength: errors[ILLEGAL_CHARACTERS].length,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ValidationFunc, ValidationError } from '../../hook_form_lib';
|
||||
import { hasMinLengthString } from '../../../validators/string';
|
||||
import { hasMinLengthArray } from '../../../validators/array';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
export const minLengthField = ({
|
||||
length = 0,
|
||||
message,
|
||||
}: {
|
||||
length: number;
|
||||
message: string | ((err: Partial<ValidationError>) => string);
|
||||
}) => (...args: Parameters<ValidationFunc>): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
// Validate for Arrays
|
||||
if (Array.isArray(value)) {
|
||||
return hasMinLengthArray(length)(value)
|
||||
? undefined
|
||||
: {
|
||||
code: 'ERR_MIN_LENGTH',
|
||||
length,
|
||||
message: typeof message === 'function' ? message({ length }) : message,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate for Strings
|
||||
return hasMinLengthString(length)((value as string).trim())
|
||||
? undefined
|
||||
: {
|
||||
code: 'ERR_MIN_LENGTH',
|
||||
length,
|
||||
message: typeof message === 'function' ? message({ length }) : message,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
|
||||
import { ValidationFunc, ValidationError } from '../../hook_form_lib';
|
||||
import { hasMinLengthArray } from '../../../validators/array';
|
||||
import { multiSelectComponent } from '../serializers';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
const { optionsToSelectedValue } = multiSelectComponent;
|
||||
|
||||
/**
|
||||
* Validator to validate that a EuiSelectable has a minimum number
|
||||
* of items selected.
|
||||
* @param total Minimum number of items
|
||||
*/
|
||||
export const minSelectableSelectionField = ({
|
||||
total = 0,
|
||||
message,
|
||||
}: {
|
||||
total: number;
|
||||
message: string | ((err: Partial<ValidationError>) => string);
|
||||
}) => (...args: Parameters<ValidationFunc>): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
// We need to convert all the options from the multi selectable component, to the
|
||||
// an actual Array of selection _before_ validating the Array length.
|
||||
return hasMinLengthArray(total)(optionsToSelectedValue(value as Option[]))
|
||||
? undefined
|
||||
: {
|
||||
code: 'ERR_MIN_SELECTION',
|
||||
total,
|
||||
message: typeof message === 'function' ? message({ length }) : message,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ValidationFunc } from '../../hook_form_lib';
|
||||
import { startsWith } from '../../../validators/string';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
export const startsWithField = ({ message, char }: { message: string; char: string }) => (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWith(char)(value)) {
|
||||
return {
|
||||
code: 'ERR_FIRST_CHAR',
|
||||
char,
|
||||
message,
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export type ERROR_CODE =
|
||||
| 'ERR_FIELD_MISSING'
|
||||
| 'ERR_FIELD_FORMAT'
|
||||
| 'ERR_INVALID_CHARS'
|
||||
| 'ERR_FIRST_CHAR'
|
||||
| 'ERR_MIN_LENGTH'
|
||||
| 'ERR_MAX_LENGTH'
|
||||
| 'ERR_MIN_SELECTION'
|
||||
| 'ERR_MAX_SELECTION';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ValidationFunc, ValidationError } from '../../hook_form_lib';
|
||||
import { isUrl } from '../../../validators/string';
|
||||
import { ERROR_CODE } from './types';
|
||||
|
||||
export const urlField = (message: string) => (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<any, ERROR_CODE>> => {
|
||||
const [{ value }] = args;
|
||||
|
||||
const error: ValidationError<ERROR_CODE> = {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
formatType: 'URL',
|
||||
message,
|
||||
};
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
return isUrl(value) ? undefined : error;
|
||||
};
|
28
src/plugins/es_ui_shared/static/forms/helpers/index.ts
Normal file
28
src/plugins/es_ui_shared/static/forms/helpers/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import * as fieldValidatorsImport from './field_validators';
|
||||
import * as fieldFormattersImport from './field_formatters';
|
||||
import * as serializersImport from './serializers';
|
||||
import * as deserializersImport from './de_serializers';
|
||||
|
||||
export const fieldValidators = fieldValidatorsImport;
|
||||
export const fieldFormatters = fieldFormattersImport;
|
||||
export const deserializers = deserializersImport;
|
||||
export const serializers = serializersImport;
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { stripEmptyFields } from './serializers';
|
||||
|
||||
describe('Serializers', () => {
|
||||
describe('stripEmptyFields()', () => {
|
||||
let object: { [key: string]: any };
|
||||
|
||||
beforeEach(() => {
|
||||
object = {
|
||||
a: '',
|
||||
b: ' ',
|
||||
c: '0',
|
||||
d: 0,
|
||||
e: false,
|
||||
f: null,
|
||||
g: [],
|
||||
h: {},
|
||||
i: {
|
||||
a: '',
|
||||
b: {},
|
||||
c: null,
|
||||
d: 123,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test('should not remove empty string or empty object in child objects (not recursively = default)', () => {
|
||||
const expected = {
|
||||
c: object.c,
|
||||
d: object.d,
|
||||
e: object.e,
|
||||
f: object.f,
|
||||
g: object.g,
|
||||
i: object.i, // not mutaded
|
||||
};
|
||||
expect(stripEmptyFields(object)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should remove all empty string and empty object (recursively)', () => {
|
||||
const expected = {
|
||||
c: object.c,
|
||||
d: object.d,
|
||||
e: object.e,
|
||||
f: object.f,
|
||||
g: object.g,
|
||||
i: {
|
||||
c: object.i.c,
|
||||
d: object.i.d,
|
||||
},
|
||||
};
|
||||
expect(stripEmptyFields(object, { recursive: true })).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should only remove empty string (recursively)', () => {
|
||||
const expected = {
|
||||
a: object.a,
|
||||
b: object.b,
|
||||
c: object.c,
|
||||
d: object.d,
|
||||
e: object.e,
|
||||
f: object.f,
|
||||
g: object.g,
|
||||
i: {
|
||||
a: object.i.a,
|
||||
c: object.i.c,
|
||||
d: object.i.d,
|
||||
},
|
||||
};
|
||||
expect(stripEmptyFields(object, { recursive: true, types: ['object'] })).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should only remove empty objects (recursively)', () => {
|
||||
const expected = {
|
||||
c: object.c,
|
||||
d: object.d,
|
||||
e: object.e,
|
||||
f: object.f,
|
||||
g: object.g,
|
||||
h: object.h,
|
||||
i: {
|
||||
b: object.i.b,
|
||||
c: object.i.c,
|
||||
d: object.i.d,
|
||||
},
|
||||
};
|
||||
expect(stripEmptyFields(object, { recursive: true, types: ['string'] })).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
92
src/plugins/es_ui_shared/static/forms/helpers/serializers.ts
Normal file
92
src/plugins/es_ui_shared/static/forms/helpers/serializers.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Output transforms are functions that will be called
|
||||
* with the form field value whenever we access the form data object. (with `form.getFormData()`)
|
||||
*
|
||||
* This allows us to have a different object/array as field `value`
|
||||
* from the desired outputed form data.
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* myField.value = [{ label: 'index_1', isSelected: true }, { label: 'index_2', isSelected: false }]
|
||||
* const serializer = (value) => (
|
||||
* value.filter(v => v.selected).map(v => v.label)
|
||||
* );
|
||||
*
|
||||
* // When serializing the form data, the following array will be returned
|
||||
* form.getFormData() -> { myField: ['index_1'] }
|
||||
* ````
|
||||
*/
|
||||
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
import { SerializerFunc } from '../hook_form_lib';
|
||||
|
||||
export const multiSelectComponent: Record<string, SerializerFunc<string[]>> = {
|
||||
/**
|
||||
* Return an array of labels of all the options that are selected
|
||||
*
|
||||
* @param value The Eui Selectable options array
|
||||
*/
|
||||
optionsToSelectedValue(options: Option[]): string[] {
|
||||
return options.filter(option => option.checked === 'on').map(option => option.label);
|
||||
},
|
||||
};
|
||||
|
||||
interface StripEmptyFieldsOptions {
|
||||
types?: Array<'string' | 'object'>;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip empty fields from a data object.
|
||||
* Empty fields can either be an empty string (one or several blank spaces) or an empty object (no keys)
|
||||
*
|
||||
* @param object Object to remove the empty fields from.
|
||||
* @param types An array of types to strip. Types can be "string" or "object". Defaults to ["string", "object"]
|
||||
* @param options An optional configuration object. By default recursive it turned on.
|
||||
*/
|
||||
export const stripEmptyFields = (
|
||||
object: { [key: string]: any },
|
||||
options?: StripEmptyFieldsOptions
|
||||
): { [key: string]: any } => {
|
||||
const { types = ['string', 'object'], recursive = false } = options || {};
|
||||
|
||||
return Object.entries(object).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const type = typeof value;
|
||||
const shouldStrip = types.includes(type as 'string');
|
||||
|
||||
if (shouldStrip && type === 'string' && value.trim() === '') {
|
||||
return acc;
|
||||
} else if (type === 'object' && !Array.isArray(value) && value !== null) {
|
||||
if (Object.keys(value).length === 0 && shouldStrip) {
|
||||
return acc;
|
||||
} else if (recursive) {
|
||||
value = stripEmptyFields({ ...value }, options);
|
||||
}
|
||||
}
|
||||
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: any }
|
||||
);
|
||||
};
|
|
@ -25,16 +25,12 @@ import { FormHook } from '../types';
|
|||
|
||||
interface Props {
|
||||
form: FormHook<any>;
|
||||
FormWrapper?: (props: any) => JSX.Element;
|
||||
FormWrapper?: React.ComponentType;
|
||||
children: ReactNode | ReactNode[];
|
||||
className: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const DefaultFormWrapper = (props: any) => {
|
||||
return <EuiForm {...props} />;
|
||||
};
|
||||
|
||||
export const Form = ({ form, FormWrapper = DefaultFormWrapper, ...rest }: Props) => (
|
||||
export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => (
|
||||
<FormProvider form={form}>
|
||||
<FormWrapper {...rest} />
|
||||
</FormProvider>
|
||||
|
|
|
@ -32,7 +32,7 @@ export const FormProvider = ({ children, form }: Props) => (
|
|||
<FormContext.Provider value={form}>{children}</FormContext.Provider>
|
||||
);
|
||||
|
||||
export const useFormContext = function<T = Record<string, unknown>>() {
|
||||
export const useFormContext = function<T extends object = Record<string, unknown>>() {
|
||||
const context = useContext(FormContext) as FormHook<T>;
|
||||
if (context === undefined) {
|
||||
throw new Error('useFormContext must be used within a <FormProvider />');
|
||||
|
|
|
@ -24,12 +24,16 @@ import { FormHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types
|
|||
import { mapFormFields, flattenObject, unflattenObject, Subject } from '../lib';
|
||||
|
||||
const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500;
|
||||
const DEFAULT_OPTIONS = {
|
||||
errorDisplayDelay: DEFAULT_ERROR_DISPLAY_TIMEOUT,
|
||||
stripEmptyFields: true,
|
||||
};
|
||||
|
||||
interface UseFormReturn<T> {
|
||||
interface UseFormReturn<T extends object> {
|
||||
form: FormHook<T>;
|
||||
}
|
||||
|
||||
export function useForm<T = FormData>(
|
||||
export function useForm<T extends object = FormData>(
|
||||
formConfig: FormConfig<T> | undefined = {}
|
||||
): UseFormReturn<T> {
|
||||
const {
|
||||
|
@ -38,8 +42,9 @@ export function useForm<T = FormData>(
|
|||
defaultValue = {},
|
||||
serializer = (data: any) => data,
|
||||
deserializer = (data: any) => data,
|
||||
options = { errorDisplayDelay: DEFAULT_ERROR_DISPLAY_TIMEOUT, stripEmptyFields: true },
|
||||
options = {},
|
||||
} = formConfig;
|
||||
const formOptions = { ...DEFAULT_OPTIONS, ...options };
|
||||
const defaultValueDeserialized =
|
||||
Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue);
|
||||
const [isSubmitted, setSubmitted] = useState(false);
|
||||
|
@ -59,7 +64,7 @@ export function useForm<T = FormData>(
|
|||
const fieldsToArray = () => Object.values(fieldsRefs.current);
|
||||
|
||||
const stripEmptyFields = (fields: FieldsMap): FieldsMap => {
|
||||
if (options.stripEmptyFields) {
|
||||
if (formOptions.stripEmptyFields) {
|
||||
return Object.entries(fields).reduce(
|
||||
(acc, [key, field]) => {
|
||||
if (typeof field.value !== 'string' || field.value.trim() !== '') {
|
||||
|
@ -191,7 +196,7 @@ export function useForm<T = FormData>(
|
|||
getFields,
|
||||
getFormData,
|
||||
getFieldDefaultValue,
|
||||
__options: options,
|
||||
__options: formOptions,
|
||||
__formData$: formData$,
|
||||
__updateFormDataAt: updateFormDataAt,
|
||||
__readFieldConfigFromSchema: readFieldConfigFromSchema,
|
||||
|
|
|
@ -17,10 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react';
|
||||
import { ReactNode, ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react';
|
||||
import { Subject } from './lib';
|
||||
|
||||
export interface FormHook<T = FormData> {
|
||||
// This type will convert all optional property to required ones
|
||||
// Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623
|
||||
type Required<T> = T extends object ? { [P in keyof T]-?: NonNullable<T[P]> } : T;
|
||||
|
||||
export interface FormHook<T extends object = FormData> {
|
||||
readonly isSubmitted: boolean;
|
||||
readonly isSubmitting: boolean;
|
||||
readonly isValid: boolean;
|
||||
|
@ -30,7 +34,7 @@ export interface FormHook<T = FormData> {
|
|||
getFields: () => FieldsMap;
|
||||
getFormData: (options?: { unflatten?: boolean }) => T;
|
||||
getFieldDefaultValue: (fieldName: string) => unknown;
|
||||
readonly __options: FormOptions;
|
||||
readonly __options: Required<FormOptions>;
|
||||
readonly __formData$: MutableRefObject<Subject<T>>;
|
||||
__addField: (field: FieldHook) => void;
|
||||
__removeField: (fieldNames: string | string[]) => void;
|
||||
|
@ -39,15 +43,15 @@ export interface FormHook<T = FormData> {
|
|||
__readFieldConfigFromSchema: (fieldName: string) => FieldConfig;
|
||||
}
|
||||
|
||||
export interface FormSchema<T = FormData> {
|
||||
export interface FormSchema<T extends object = FormData> {
|
||||
[key: string]: FormSchemaEntry<T>;
|
||||
}
|
||||
type FormSchemaEntry<T> =
|
||||
type FormSchemaEntry<T extends object> =
|
||||
| FieldConfig<T>
|
||||
| Array<FieldConfig<T>>
|
||||
| { [key: string]: FieldConfig<T> | Array<FieldConfig<T>> | FormSchemaEntry<T> };
|
||||
|
||||
export interface FormConfig<T = FormData> {
|
||||
export interface FormConfig<T extends object = FormData> {
|
||||
onSubmit?: (data: T, isFormValid: boolean) => void;
|
||||
schema?: FormSchema<T>;
|
||||
defaultValue?: Partial<T>;
|
||||
|
@ -57,17 +61,17 @@ export interface FormConfig<T = FormData> {
|
|||
}
|
||||
|
||||
export interface FormOptions {
|
||||
errorDisplayDelay: number;
|
||||
errorDisplayDelay?: number;
|
||||
/**
|
||||
* Remove empty string field ("") from form data
|
||||
*/
|
||||
stripEmptyFields: boolean;
|
||||
stripEmptyFields?: boolean;
|
||||
}
|
||||
|
||||
export interface FieldHook {
|
||||
readonly path: string;
|
||||
readonly label?: string;
|
||||
readonly helpText?: string;
|
||||
readonly helpText?: string | ReactNode;
|
||||
readonly type: string;
|
||||
readonly value: unknown;
|
||||
readonly errors: ValidationError[];
|
||||
|
@ -91,10 +95,10 @@ export interface FieldHook {
|
|||
__serializeOutput: (rawValue?: unknown) => unknown;
|
||||
}
|
||||
|
||||
export interface FieldConfig<T = any> {
|
||||
export interface FieldConfig<T extends object = any> {
|
||||
readonly path?: string;
|
||||
readonly label?: string;
|
||||
readonly helpText?: string;
|
||||
readonly helpText?: string | ReactNode;
|
||||
readonly type?: HTMLInputElement['type'];
|
||||
readonly defaultValue?: unknown;
|
||||
readonly validations?: Array<ValidationConfig<T>>;
|
||||
|
@ -111,20 +115,20 @@ export interface FieldsMap {
|
|||
|
||||
export type FormSubmitHandler<T> = (formData: T, isValid: boolean) => Promise<void>;
|
||||
|
||||
export interface ValidationError {
|
||||
message: string | ((error: ValidationError) => string);
|
||||
code?: string;
|
||||
export interface ValidationError<T = string> {
|
||||
message: string;
|
||||
code?: T;
|
||||
validationType?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type ValidationFunc<T = any> = (data: {
|
||||
export type ValidationFunc<T extends object = any, E = string> = (data: {
|
||||
path: string;
|
||||
value: unknown;
|
||||
form: FormHook<T>;
|
||||
formData: T;
|
||||
errors: readonly ValidationError[];
|
||||
}) => ValidationError | void | undefined | Promise<ValidationError | void | undefined>;
|
||||
}) => ValidationError<E> | void | undefined | Promise<ValidationError<E> | void | undefined>;
|
||||
|
||||
export interface FieldValidateResponse {
|
||||
isValid: boolean;
|
||||
|
@ -133,7 +137,9 @@ export interface FieldValidateResponse {
|
|||
|
||||
export type SerializerFunc<T = unknown> = (value: any) => T;
|
||||
|
||||
export type FormData = Record<string, unknown>;
|
||||
export interface FormData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type FormatterFunc = (value: any) => unknown;
|
||||
|
||||
|
@ -141,7 +147,7 @@ type FormatterFunc = (value: any) => unknown;
|
|||
// string | number | boolean | string[] ...
|
||||
type FieldValue = unknown;
|
||||
|
||||
export interface ValidationConfig<T = any> {
|
||||
export interface ValidationConfig<T extends object = any> {
|
||||
validator: ValidationFunc<T>;
|
||||
type?: string;
|
||||
exitOnFail?: boolean;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const hasMaxLengthArray = (length = 5) => (value: any[]): boolean => value.length <= length;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const hasMinLengthArray = (length = 1) => (value: any[]): boolean => value.length >= length;
|
22
src/plugins/es_ui_shared/static/validators/array/index.ts
Normal file
22
src/plugins/es_ui_shared/static/validators/array/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './has_max_length';
|
||||
export * from './has_min_length';
|
||||
export * from './is_empty';
|
20
src/plugins/es_ui_shared/static/validators/array/is_empty.ts
Normal file
20
src/plugins/es_ui_shared/static/validators/array/is_empty.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const isEmptyArray = (value: any[]): boolean => value.length === 0;
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const containsChars = (chars: string | string[]) => (value: string) => {
|
||||
const charToArray = Array.isArray(chars) ? (chars as string[]) : ([chars] as string[]);
|
||||
|
||||
const charsFound = charToArray.reduce(
|
||||
(acc, char) => (value.includes(char) ? [...acc, char] : acc),
|
||||
[] as string[]
|
||||
);
|
||||
|
||||
return {
|
||||
charsFound,
|
||||
doesContain: charsFound.length > 0,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const endsWith = (char: string) => (value: string) => value.endsWith(char);
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const hasMaxLengthString = (length: number) => (str: string) => str.length <= length;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const hasMinLengthString = (length: number) => (str: string) => str.length >= length;
|
27
src/plugins/es_ui_shared/static/validators/string/index.ts
Normal file
27
src/plugins/es_ui_shared/static/validators/string/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './contains_chars';
|
||||
export * from './ends_with';
|
||||
export * from './has_max_length';
|
||||
export * from './has_min_length';
|
||||
export * from './is_empty';
|
||||
export * from './is_url';
|
||||
export * from './starts_with';
|
||||
export * from './is_json';
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const isEmptyString = (value: string) => value.trim() === '';
|
30
src/plugins/es_ui_shared/static/validators/string/is_json.ts
Normal file
30
src/plugins/es_ui_shared/static/validators/string/is_json.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const isJSON = (value: string) => {
|
||||
try {
|
||||
const parsedJSON = JSON.parse(value);
|
||||
if (parsedJSON && typeof parsedJSON !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
47
src/plugins/es_ui_shared/static/validators/string/is_url.ts
Normal file
47
src/plugins/es_ui_shared/static/validators/string/is_url.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
|
||||
const localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/;
|
||||
const nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/;
|
||||
|
||||
export const isUrl = (string: string) => {
|
||||
if (typeof string !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = string.match(protocolAndDomainRE);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const everythingAfterProtocol = match[1];
|
||||
if (!everythingAfterProtocol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
localhostDomainRE.test(everythingAfterProtocol) ||
|
||||
nonLocalhostDomainRE.test(everythingAfterProtocol)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const startsWith = (char: string) => (value: string) => value.startsWith(char);
|
4
typings/@elastic/eui/index.d.ts
vendored
4
typings/@elastic/eui/index.d.ts
vendored
|
@ -22,6 +22,10 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction';
|
|||
|
||||
declare module '@elastic/eui' {
|
||||
export const EuiSideNav: React.SFC<any>;
|
||||
export const EuiDescribedFormGroup: React.SFC<any>;
|
||||
export const EuiCodeEditor: React.SFC<any>;
|
||||
export const Query: any;
|
||||
export const EuiCard: any;
|
||||
|
||||
export interface EuiTableCriteria {
|
||||
page: { index: number; size: number };
|
||||
|
|
20
typings/@elastic/eui/lib/format.d.ts
vendored
Normal file
20
typings/@elastic/eui/lib/format.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const dateFormatAliases: any;
|
20
typings/@elastic/eui/lib/services.d.ts
vendored
Normal file
20
typings/@elastic/eui/lib/services.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const RIGHT_ALIGNMENT: any;
|
|
@ -86,7 +86,7 @@ export const init = () => {
|
|||
// Define default response for unhandled requests.
|
||||
// We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
|
||||
// and we can mock them all with a 200 instead of mocking each one individually.
|
||||
server.respondWith([200, {}, 'DefaultResponse']);
|
||||
server.respondWith([200, {}, 'DefaultSinonMockServerResponse']);
|
||||
|
||||
const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { TemplateCreate } from '../../../public/sections/template_create';
|
||||
import { formSetup } from './template_form.helpers';
|
||||
import { formSetup, TestSubjects } from './template_form.helpers';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
|
@ -17,6 +17,6 @@ const testBedConfig: TestBedConfig = {
|
|||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(TemplateCreate, testBedConfig);
|
||||
const initTestBed = registerTestBed<TestSubjects>(TemplateCreate, testBedConfig);
|
||||
|
||||
export const setup = formSetup.bind(null, initTestBed);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
|
||||
import { BASE_PATH } from '../../../common/constants';
|
||||
import { TemplateEdit } from '../../../public/sections/template_edit';
|
||||
import { formSetup } from './template_form.helpers';
|
||||
import { formSetup, TestSubjects } from './template_form.helpers';
|
||||
import { TEMPLATE_NAME } from './constants';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
|
@ -18,6 +18,6 @@ const testBedConfig: TestBedConfig = {
|
|||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(TemplateEdit, testBedConfig);
|
||||
const initTestBed = registerTestBed<TestSubjects>(TemplateEdit, testBedConfig);
|
||||
|
||||
export const setup = formSetup.bind(null, initTestBed);
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TestBed } from '../../../../../../test_utils';
|
||||
import { TestBed, SetupFunc } from '../../../../../../test_utils';
|
||||
import { Template } from '../../../common/types';
|
||||
import { nextTick } from './index';
|
||||
|
||||
export interface TemplateFormTestBed extends TestBed<TemplateFormTestSubjects> {
|
||||
actions: {
|
||||
|
@ -13,14 +14,16 @@ export interface TemplateFormTestBed extends TestBed<TemplateFormTestSubjects> {
|
|||
clickBackButton: () => void;
|
||||
clickSubmitButton: () => void;
|
||||
completeStepOne: ({ name, indexPatterns, order, version }: Partial<Template>) => void;
|
||||
completeStepTwo: ({ settings }: Partial<Template>) => void;
|
||||
completeStepThree: ({ mappings }: Partial<Template>) => void;
|
||||
completeStepFour: ({ aliases }: Partial<Template>) => void;
|
||||
completeStepTwo: (settings: string) => void;
|
||||
completeStepThree: (mappings: string) => void;
|
||||
completeStepFour: (aliases: string) => void;
|
||||
selectSummaryTab: (tab: 'summary' | 'request') => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const formSetup = async (initTestBed: any): Promise<TemplateFormTestBed> => {
|
||||
export const formSetup = async (
|
||||
initTestBed: SetupFunc<TestSubjects>
|
||||
): Promise<TemplateFormTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
// User actions
|
||||
|
@ -36,11 +39,11 @@ export const formSetup = async (initTestBed: any): Promise<TemplateFormTestBed>
|
|||
testBed.find('submitButton').simulate('click');
|
||||
};
|
||||
|
||||
const completeStepOne = ({ name, indexPatterns, order, version }: Partial<Template>) => {
|
||||
const { form, find } = testBed;
|
||||
const completeStepOne = async ({ name, indexPatterns, order, version }: Partial<Template>) => {
|
||||
const { form, find, component } = testBed;
|
||||
|
||||
if (name) {
|
||||
form.setInputValue('nameInput', name);
|
||||
form.setInputValue('nameField.input', name);
|
||||
}
|
||||
|
||||
if (indexPatterns) {
|
||||
|
@ -50,53 +53,68 @@ export const formSetup = async (initTestBed: any): Promise<TemplateFormTestBed>
|
|||
}));
|
||||
|
||||
find('mockComboBox').simulate('change', indexPatternsFormatted); // Using mocked EuiComboBox
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
if (order) {
|
||||
form.setInputValue('orderInput', JSON.stringify(order));
|
||||
form.setInputValue('orderField.input', JSON.stringify(order));
|
||||
}
|
||||
|
||||
if (version) {
|
||||
form.setInputValue('versionInput', JSON.stringify(version));
|
||||
form.setInputValue('versionField.input', JSON.stringify(version));
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
};
|
||||
|
||||
const completeStepTwo = ({ settings }: Partial<Template>) => {
|
||||
const { find } = testBed;
|
||||
const completeStepTwo = async (settings: string) => {
|
||||
const { find, component } = testBed;
|
||||
|
||||
if (settings) {
|
||||
find('mockCodeEditor').simulate('change', {
|
||||
jsonString: settings,
|
||||
}); // Using mocked EuiCodeEditor
|
||||
await nextTick();
|
||||
component.update();
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
};
|
||||
|
||||
const completeStepThree = ({ mappings }: Partial<Template>) => {
|
||||
const { find } = testBed;
|
||||
const completeStepThree = async (mappings: string) => {
|
||||
const { find, component } = testBed;
|
||||
|
||||
if (mappings) {
|
||||
find('mockCodeEditor').simulate('change', {
|
||||
jsonString: mappings,
|
||||
}); // Using mocked EuiCodeEditor
|
||||
await nextTick(50);
|
||||
component.update();
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
await nextTick(50); // hooks updates cycles are tricky, adding some latency is needed
|
||||
component.update();
|
||||
};
|
||||
|
||||
const completeStepFour = ({ aliases }: Partial<Template>) => {
|
||||
const { find } = testBed;
|
||||
const completeStepFour = async (aliases: string) => {
|
||||
const { find, component } = testBed;
|
||||
|
||||
if (aliases) {
|
||||
find('mockCodeEditor').simulate('change', {
|
||||
jsonString: aliases,
|
||||
}); // Using mocked EuiCodeEditor
|
||||
await nextTick(50);
|
||||
component.update();
|
||||
}
|
||||
|
||||
clickNextButton();
|
||||
await nextTick(50);
|
||||
component.update();
|
||||
};
|
||||
|
||||
const selectSummaryTab = (tab: 'summary' | 'request') => {
|
||||
|
@ -126,17 +144,19 @@ export const formSetup = async (initTestBed: any): Promise<TemplateFormTestBed>
|
|||
|
||||
export type TemplateFormTestSubjects = TestSubjects;
|
||||
|
||||
type TestSubjects =
|
||||
export type TestSubjects =
|
||||
| 'backButton'
|
||||
| 'codeEditorContainer'
|
||||
| 'indexPatternsComboBox'
|
||||
| 'indexPatternsField'
|
||||
| 'indexPatternsWarning'
|
||||
| 'indexPatternsWarningDescription'
|
||||
| 'mockCodeEditor'
|
||||
| 'mockComboBox'
|
||||
| 'nameInput'
|
||||
| 'nameField'
|
||||
| 'nameField.input'
|
||||
| 'nextButton'
|
||||
| 'orderInput'
|
||||
| 'orderField'
|
||||
| 'orderField.input'
|
||||
| 'pageTitle'
|
||||
| 'requestTab'
|
||||
| 'saveTemplateError'
|
||||
|
@ -153,4 +173,5 @@ type TestSubjects =
|
|||
| 'templateForm'
|
||||
| 'templateFormContainer'
|
||||
| 'testingEditor'
|
||||
| 'versionInput';
|
||||
| 'versionField'
|
||||
| 'versionField.input';
|
||||
|
|
|
@ -130,14 +130,14 @@ describe.skip('<IndexManagementHome />', () => {
|
|||
const template1 = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
settings: JSON.stringify({
|
||||
settings: {
|
||||
index: {
|
||||
number_of_shards: '1',
|
||||
lifecycle: {
|
||||
name: 'my_ilm_policy',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
const template2 = fixtures.getTemplate({
|
||||
name: `b${getRandomString()}`,
|
||||
|
@ -415,12 +415,12 @@ describe.skip('<IndexManagementHome />', () => {
|
|||
const template = fixtures.getTemplate({
|
||||
name: `a${getRandomString()}`,
|
||||
indexPatterns: ['template1Pattern1*', 'template1Pattern2'],
|
||||
settings: JSON.stringify({
|
||||
settings: {
|
||||
index: {
|
||||
number_of_shards: '1',
|
||||
},
|
||||
}),
|
||||
mappings: JSON.stringify({
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false,
|
||||
},
|
||||
|
@ -430,10 +430,10 @@ describe.skip('<IndexManagementHome />', () => {
|
|||
format: 'EEE MMM dd HH:mm:ss Z yyyy',
|
||||
},
|
||||
},
|
||||
}),
|
||||
aliases: JSON.stringify({
|
||||
},
|
||||
aliases: {
|
||||
alias1: {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { find, actions, exists } = testBed;
|
||||
|
|
|
@ -77,10 +77,10 @@ describe.skip('<TemplateClone />', () => {
|
|||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(templateToClone);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
|
@ -97,22 +97,31 @@ describe.skip('<TemplateClone />', () => {
|
|||
|
||||
describe('form payload', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions } = testBed;
|
||||
const { actions, component } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
// Specify index patterns, but do not change name (keep default)
|
||||
actions.completeStepOne({
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
// Specify index patterns, but do not change name (keep default)
|
||||
await actions.completeStepOne({
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
});
|
||||
|
||||
// Bypass step 2 (index settings)
|
||||
actions.clickNextButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
|
||||
// Bypass step 3 (mappings)
|
||||
actions.clickNextButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
|
||||
// Bypass step 4 (aliases)
|
||||
actions.clickNextButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
// Bypass step 2 (index settings)
|
||||
actions.clickNextButton();
|
||||
|
||||
// Bypass step 3 (mappings)
|
||||
actions.clickNextButton();
|
||||
|
||||
// Bypass step 4 (aliases)
|
||||
actions.clickNextButton();
|
||||
});
|
||||
|
||||
it('should send the correct payload', async () => {
|
||||
|
@ -126,13 +135,16 @@ describe.skip('<TemplateClone />', () => {
|
|||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
...templateToClone,
|
||||
name: `${templateToClone.name}-copy`,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
})
|
||||
);
|
||||
const body = JSON.parse(latestRequest.requestBody);
|
||||
const expected = {
|
||||
...templateToClone,
|
||||
name: `${templateToClone.name}-copy`,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
aliases: {},
|
||||
mappings: {},
|
||||
settings: {},
|
||||
};
|
||||
expect(body).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ jest.mock('@elastic/eui', () => ({
|
|||
EuiCodeEditor: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockCodeEditor"
|
||||
onChange={async (syntheticEvent: any) => {
|
||||
onChange={(syntheticEvent: any) => {
|
||||
props.onChange(syntheticEvent.jsonString);
|
||||
}}
|
||||
/>
|
||||
|
@ -88,14 +88,19 @@ describe.skip('<TemplateCreate />', () => {
|
|||
expect(find('pageTitle').text()).toEqual('Create template');
|
||||
});
|
||||
|
||||
test('should not let the user go to the next step with invalid fields', () => {
|
||||
const { find, form } = testBed;
|
||||
test('should not let the user go to the next step with invalid fields', async () => {
|
||||
const { find, actions, component } = testBed;
|
||||
|
||||
form.setInputValue('nameInput', '');
|
||||
find('mockComboBox').simulate('change', [{ value: '' }]);
|
||||
expect(find('nextButton').props().disabled).toEqual(false);
|
||||
|
||||
const nextButton = find('nextButton');
|
||||
expect(nextButton.props().disabled).toEqual(true);
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickNextButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(find('nextButton').props().disabled).toEqual(true);
|
||||
});
|
||||
|
||||
describe('form validation', () => {
|
||||
|
@ -104,21 +109,29 @@ describe.skip('<TemplateCreate />', () => {
|
|||
});
|
||||
|
||||
describe('index settings (step 2)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the correct page title', async () => {
|
||||
const { exists, find } = testBed;
|
||||
|
||||
expect(exists('stepSettings')).toBe(true);
|
||||
expect(find('stepTitle').text()).toEqual('Index settings (optional)');
|
||||
});
|
||||
|
||||
it('should not allow invalid json', async () => {
|
||||
const { form, actions, exists, find } = testBed;
|
||||
const { form, actions } = testBed;
|
||||
|
||||
// Complete step 2 (index settings) with invalid json
|
||||
expect(exists('stepSettings')).toBe(true);
|
||||
expect(find('stepTitle').text()).toEqual('Index settings (optional)');
|
||||
actions.completeStepTwo({
|
||||
settings: '{ invalidJsonString ',
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.completeStepTwo('{ invalidJsonString ');
|
||||
});
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
|
||||
|
@ -126,26 +139,33 @@ describe.skip('<TemplateCreate />', () => {
|
|||
});
|
||||
|
||||
describe('mappings (step 3)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
actions.completeStepTwo({
|
||||
settings: '{}',
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepTwo('{}');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow invalid json', async () => {
|
||||
const { actions, form, exists, find } = testBed;
|
||||
it('should set the correct page title', async () => {
|
||||
const { exists, find } = testBed;
|
||||
|
||||
// Complete step 3 (mappings) with invalid json
|
||||
expect(exists('stepMappings')).toBe(true);
|
||||
expect(find('stepTitle').text()).toEqual('Mappings (optional)');
|
||||
actions.completeStepThree({
|
||||
mappings: '{ invalidJsonString ',
|
||||
});
|
||||
|
||||
it('should not allow invalid json', async () => {
|
||||
const { actions, form } = testBed;
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 3 (mappings) with invalid json
|
||||
await actions.completeStepThree('{ invalidJsonString ');
|
||||
});
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
|
||||
|
@ -153,31 +173,36 @@ describe.skip('<TemplateCreate />', () => {
|
|||
});
|
||||
|
||||
describe('aliases (step 4)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
actions.completeStepTwo({
|
||||
settings: '{}',
|
||||
});
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepTwo('{}');
|
||||
|
||||
// Complete step 3 (mappings)
|
||||
actions.completeStepThree({
|
||||
mappings: '{}',
|
||||
// Complete step 3 (mappings)
|
||||
await actions.completeStepThree('{}');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow invalid json', async () => {
|
||||
const { actions, form, exists, find } = testBed;
|
||||
it('should set the correct page title', async () => {
|
||||
const { exists, find } = testBed;
|
||||
|
||||
// Complete step 4 (aliases) with invalid json
|
||||
expect(exists('stepAliases')).toBe(true);
|
||||
expect(find('stepTitle').text()).toEqual('Aliases (optional)');
|
||||
actions.completeStepFour({
|
||||
aliases: '{ invalidJsonString ',
|
||||
});
|
||||
|
||||
it('should not allow invalid json', async () => {
|
||||
const { actions, form } = testBed;
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 4 (aliases) with invalid json
|
||||
await actions.completeStepFour('{ invalidJsonString ');
|
||||
});
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
|
||||
|
@ -191,25 +216,22 @@ describe.skip('<TemplateCreate />', () => {
|
|||
|
||||
const { actions } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
});
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
});
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
actions.completeStepTwo({
|
||||
settings: JSON.stringify(SETTINGS),
|
||||
});
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepTwo(JSON.stringify(SETTINGS));
|
||||
|
||||
// Complete step 3 (mappings)
|
||||
actions.completeStepThree({
|
||||
mappings: JSON.stringify(MAPPINGS),
|
||||
});
|
||||
// Complete step 3 (mappings)
|
||||
await actions.completeStepThree(JSON.stringify(MAPPINGS));
|
||||
|
||||
// Complete step 4 (aliases)
|
||||
actions.completeStepFour({
|
||||
aliases: JSON.stringify(ALIASES),
|
||||
// Complete step 4 (aliases)
|
||||
await actions.completeStepFour(JSON.stringify(ALIASES));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -249,25 +271,22 @@ describe.skip('<TemplateCreate />', () => {
|
|||
|
||||
const { actions, exists, find } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: ['*'], // Set wildcard index pattern
|
||||
});
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: ['*'], // Set wildcard index pattern
|
||||
});
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
actions.completeStepTwo({
|
||||
settings: JSON.stringify({}),
|
||||
});
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepTwo(JSON.stringify({}));
|
||||
|
||||
// Complete step 3 (mappings)
|
||||
actions.completeStepThree({
|
||||
mappings: JSON.stringify({}),
|
||||
});
|
||||
// Complete step 3 (mappings)
|
||||
await actions.completeStepThree(JSON.stringify({}));
|
||||
|
||||
// Complete step 4 (aliases)
|
||||
actions.completeStepFour({
|
||||
aliases: JSON.stringify({}),
|
||||
// Complete step 4 (aliases)
|
||||
await actions.completeStepFour(JSON.stringify({}));
|
||||
});
|
||||
|
||||
expect(exists('indexPatternsWarning')).toBe(true);
|
||||
|
@ -283,25 +302,22 @@ describe.skip('<TemplateCreate />', () => {
|
|||
|
||||
const { actions } = testBed;
|
||||
|
||||
// Complete step 1 (logistics)
|
||||
actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
});
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
});
|
||||
|
||||
// Complete step 2 (index settings)
|
||||
actions.completeStepTwo({
|
||||
settings: JSON.stringify(SETTINGS),
|
||||
});
|
||||
// Complete step 2 (index settings)
|
||||
await actions.completeStepTwo(JSON.stringify(SETTINGS));
|
||||
|
||||
// Complete step 3 (mappings)
|
||||
actions.completeStepThree({
|
||||
mappings: JSON.stringify(MAPPINGS),
|
||||
});
|
||||
// Complete step 3 (mappings)
|
||||
await actions.completeStepThree(JSON.stringify(MAPPINGS));
|
||||
|
||||
// Complete step 4 (aliases)
|
||||
actions.completeStepFour({
|
||||
aliases: JSON.stringify(ALIASES),
|
||||
// Complete step 4 (aliases)
|
||||
await actions.completeStepFour(JSON.stringify(ALIASES));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -316,18 +332,15 @@ describe.skip('<TemplateCreate />', () => {
|
|||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
version: '',
|
||||
order: '',
|
||||
settings: JSON.stringify(SETTINGS),
|
||||
mappings: JSON.stringify(MAPPINGS),
|
||||
aliases: JSON.stringify(ALIASES),
|
||||
isManaged: false,
|
||||
})
|
||||
);
|
||||
const expected = {
|
||||
name: TEMPLATE_NAME,
|
||||
indexPatterns: DEFAULT_INDEX_PATTERNS,
|
||||
settings: SETTINGS,
|
||||
mappings: MAPPINGS,
|
||||
aliases: ALIASES,
|
||||
isManaged: false,
|
||||
};
|
||||
expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should surface the API errors from the put HTTP request', async () => {
|
||||
|
|
|
@ -55,7 +55,7 @@ jest.mock('@elastic/eui', () => ({
|
|||
EuiCodeEditor: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockCodeEditor"
|
||||
onChange={async (syntheticEvent: any) => {
|
||||
onChange={(syntheticEvent: any) => {
|
||||
props.onChange(syntheticEvent.jsonString);
|
||||
}}
|
||||
/>
|
||||
|
@ -79,10 +79,10 @@ describe.skip('<TemplateEdit />', () => {
|
|||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
|
||||
httpRequestsMockHelpers.setLoadTemplateResponse(templateToEdit);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
|
@ -98,31 +98,32 @@ describe.skip('<TemplateEdit />', () => {
|
|||
expect(find('pageTitle').text()).toEqual(`Edit template '${name}'`);
|
||||
});
|
||||
|
||||
it('should set the nameField to readOnly', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
const nameInput = find('nameField.input');
|
||||
expect(nameInput.props().disabled).toEqual(true);
|
||||
});
|
||||
|
||||
describe('form payload', () => {
|
||||
beforeEach(async () => {
|
||||
const { actions, find } = testBed;
|
||||
const { actions } = testBed;
|
||||
|
||||
// Step 1 (logistics)
|
||||
const nameInput = find('nameInput');
|
||||
expect(nameInput.props().readOnly).toEqual(true);
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
// Complete step 1 (logistics)
|
||||
await actions.completeStepOne({
|
||||
indexPatterns: UPDATED_INDEX_PATTERN,
|
||||
});
|
||||
|
||||
actions.completeStepOne({
|
||||
indexPatterns: UPDATED_INDEX_PATTERN,
|
||||
});
|
||||
// Step 2 (index settings)
|
||||
await actions.completeStepTwo(JSON.stringify(SETTINGS));
|
||||
|
||||
// Step 2 (index settings)
|
||||
actions.completeStepTwo({
|
||||
settings: JSON.stringify(SETTINGS),
|
||||
});
|
||||
// Step 3 (mappings)
|
||||
await actions.completeStepThree(JSON.stringify(MAPPINGS));
|
||||
|
||||
// Step 3 (mappings)
|
||||
actions.completeStepThree({
|
||||
mappings: JSON.stringify(MAPPINGS),
|
||||
});
|
||||
|
||||
// Step 4 (aliases)
|
||||
actions.completeStepFour({
|
||||
aliases: JSON.stringify(ALIASES),
|
||||
// Step 4 (aliases)
|
||||
await actions.completeStepFour(JSON.stringify(ALIASES));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -139,17 +140,17 @@ describe.skip('<TemplateEdit />', () => {
|
|||
|
||||
const { version, order } = templateToEdit;
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
name: TEMPLATE_NAME,
|
||||
version,
|
||||
order,
|
||||
indexPatterns: UPDATED_INDEX_PATTERN,
|
||||
settings: JSON.stringify(SETTINGS),
|
||||
mappings: JSON.stringify(MAPPINGS),
|
||||
aliases: JSON.stringify(ALIASES),
|
||||
})
|
||||
);
|
||||
const expected = {
|
||||
name: TEMPLATE_NAME,
|
||||
version,
|
||||
order,
|
||||
indexPatterns: UPDATED_INDEX_PATTERN,
|
||||
settings: SETTINGS,
|
||||
mappings: MAPPINGS,
|
||||
aliases: ALIASES,
|
||||
isManaged: false,
|
||||
};
|
||||
expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
export { PLUGIN } from './plugin';
|
||||
export { BASE_PATH } from './base_path';
|
||||
export { INVALID_INDEX_PATTERN_CHARS } from './invalid_characters';
|
||||
export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters';
|
||||
export * from './index_statuses';
|
||||
|
||||
export {
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
*/
|
||||
|
||||
export const INVALID_INDEX_PATTERN_CHARS = ['\\', '/', '?', '"', '<', '>', '|'];
|
||||
|
||||
export const INVALID_TEMPLATE_NAME_CHARS = ['"', '*', '\\', ',', '?'];
|
||||
|
|
|
@ -5,30 +5,8 @@
|
|||
*/
|
||||
import { Template, TemplateEs, TemplateListItem } from '../types';
|
||||
|
||||
const parseJson = (jsonString: string) => {
|
||||
let parsedJson;
|
||||
|
||||
try {
|
||||
parsedJson = JSON.parse(jsonString);
|
||||
|
||||
// Do not send empty object
|
||||
if (!hasEntries(parsedJson)) {
|
||||
parsedJson = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently swallow parsing errors since parsing validation is done on client
|
||||
// so we should never reach this point
|
||||
}
|
||||
|
||||
return parsedJson;
|
||||
};
|
||||
|
||||
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
|
||||
|
||||
const stringifyJson = (json: any) => {
|
||||
return JSON.stringify(json, null, 2);
|
||||
};
|
||||
|
||||
export function deserializeTemplateList(
|
||||
indexTemplatesByName: any,
|
||||
managedTemplatePrefix?: string
|
||||
|
@ -66,12 +44,12 @@ export function serializeTemplate(template: Template): TemplateEs {
|
|||
|
||||
const serializedTemplate: TemplateEs = {
|
||||
name,
|
||||
version: version ? Number(version) : undefined,
|
||||
order: order ? Number(order) : undefined,
|
||||
version,
|
||||
order,
|
||||
index_patterns: indexPatterns,
|
||||
settings: settings ? parseJson(settings) : undefined,
|
||||
aliases: aliases ? parseJson(aliases) : undefined,
|
||||
mappings: mappings ? parseJson(mappings) : undefined,
|
||||
settings,
|
||||
aliases,
|
||||
mappings,
|
||||
};
|
||||
|
||||
return serializedTemplate;
|
||||
|
@ -93,12 +71,12 @@ export function deserializeTemplate(
|
|||
|
||||
const deserializedTemplate: Template = {
|
||||
name,
|
||||
version: version || version === 0 ? version : '',
|
||||
order: order || order === 0 ? order : '',
|
||||
version,
|
||||
order,
|
||||
indexPatterns: indexPatterns.sort(),
|
||||
settings: hasEntries(settings) ? stringifyJson(settings) : undefined,
|
||||
aliases: hasEntries(aliases) ? stringifyJson(aliases) : undefined,
|
||||
mappings: hasEntries(mappings) ? stringifyJson(mappings) : undefined,
|
||||
settings,
|
||||
aliases,
|
||||
mappings,
|
||||
ilmPolicy: settings && settings.index && settings.index.lifecycle,
|
||||
isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
|
||||
};
|
||||
|
|
|
@ -20,11 +20,11 @@ export interface TemplateListItem {
|
|||
export interface Template {
|
||||
name: string;
|
||||
indexPatterns: string[];
|
||||
version?: number | '';
|
||||
order?: number | '';
|
||||
settings?: string;
|
||||
aliases?: string;
|
||||
mappings?: string;
|
||||
version?: number;
|
||||
order?: number;
|
||||
settings?: object;
|
||||
aliases?: object;
|
||||
mappings?: object;
|
||||
ilmPolicy?: {
|
||||
name: string;
|
||||
};
|
||||
|
|
|
@ -20,14 +20,19 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { templatesDocumentationLink } from '../../../lib/documentation_links';
|
||||
import { StepProps } from '../types';
|
||||
import { useJsonStep } from './use_json_step';
|
||||
|
||||
export const StepAliases: React.FunctionComponent<StepProps> = ({
|
||||
template,
|
||||
updateTemplate,
|
||||
errors,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
}) => {
|
||||
const { aliases } = template;
|
||||
const { aliases: aliasesError } = errors;
|
||||
const { content, setContent, error } = useJsonStep({
|
||||
prop: 'aliases',
|
||||
defaultValue: template.aliases,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-test-subj="stepAliases">
|
||||
|
@ -95,8 +100,8 @@ export const StepAliases: React.FunctionComponent<StepProps> = ({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(aliasesError)}
|
||||
error={aliasesError}
|
||||
isInvalid={Boolean(error)}
|
||||
error={error}
|
||||
fullWidth
|
||||
>
|
||||
<EuiCodeEditor
|
||||
|
@ -119,9 +124,9 @@ export const StepAliases: React.FunctionComponent<StepProps> = ({
|
|||
defaultMessage: 'Aliases code editor',
|
||||
}
|
||||
)}
|
||||
value={aliases}
|
||||
onChange={(newAliases: string) => {
|
||||
updateTemplate({ aliases: newAliases });
|
||||
value={content}
|
||||
onChange={(updated: string) => {
|
||||
setContent(updated);
|
||||
}}
|
||||
data-test-subj="aliasesEditor"
|
||||
/>
|
||||
|
|
|
@ -3,52 +3,100 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiFieldNumber,
|
||||
} from '@elastic/eui';
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Template } from '../../../../common/types';
|
||||
import { INVALID_INDEX_PATTERN_CHARS } from '../../../../common/constants';
|
||||
import {
|
||||
useForm,
|
||||
Form,
|
||||
UseField,
|
||||
} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
import { FormRow } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components';
|
||||
import { templatesDocumentationLink } from '../../../lib/documentation_links';
|
||||
import { StepProps } from '../types';
|
||||
import { schemas } from '../template_form_schemas';
|
||||
|
||||
const indexPatternInvalidCharacters = INVALID_INDEX_PATTERN_CHARS.join(' ');
|
||||
const i18n = {
|
||||
name: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.nameTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.nameDescription"
|
||||
defaultMessage="A unique identifier for this template."
|
||||
/>
|
||||
),
|
||||
},
|
||||
indexPatterns: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle"
|
||||
defaultMessage="Index patterns"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription"
|
||||
defaultMessage="The index patterns to apply to the template."
|
||||
/>
|
||||
),
|
||||
},
|
||||
order: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.orderTitle"
|
||||
defaultMessage="Merge order"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.orderDescription"
|
||||
defaultMessage="The merge order when multiple templates match an index."
|
||||
/>
|
||||
),
|
||||
},
|
||||
version: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.versionTitle"
|
||||
defaultMessage="Version"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.versionDescription"
|
||||
defaultMessage="A number that identifies the template to external management systems."
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const StepLogistics: React.FunctionComponent<StepProps> = ({
|
||||
template,
|
||||
updateTemplate,
|
||||
errors,
|
||||
isEditing,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
}) => {
|
||||
const { name, order, version, indexPatterns } = template;
|
||||
const { name: nameError, indexPatterns: indexPatternsError } = errors;
|
||||
|
||||
// hooks
|
||||
const [allIndexPatterns, setAllIndexPatterns] = useState<Template['indexPatterns']>([]);
|
||||
const [touchedFields, setTouchedFields] = useState({
|
||||
name: false,
|
||||
indexPatterns: false,
|
||||
const { form } = useForm({
|
||||
schema: schemas.logistics,
|
||||
defaultValue: template,
|
||||
options: { stripEmptyFields: false },
|
||||
});
|
||||
|
||||
const indexPatternOptions = indexPatterns
|
||||
? indexPatterns.map(pattern => ({ label: pattern, value: pattern }))
|
||||
: [];
|
||||
useEffect(() => {
|
||||
onStepValidityChange(form.isValid);
|
||||
}, [form.isValid]);
|
||||
|
||||
const { name: isNameTouched, indexPatterns: isIndexPatternsTouched } = touchedFields;
|
||||
useEffect(() => {
|
||||
setDataGetter(form.submit);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="stepLogistics">
|
||||
<Form form={form} data-test-subj="stepLogistics">
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
|
@ -78,197 +126,54 @@ export const StepLogistics: React.FunctionComponent<StepProps> = ({
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
{/* Name */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.nameTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.nameDescription"
|
||||
defaultMessage="A unique identifier for this template."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsNameDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.fieldNameLabel"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
}
|
||||
isInvalid={isNameTouched && Boolean(nameError)}
|
||||
error={nameError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
value={name}
|
||||
readOnly={isEditing}
|
||||
onBlur={() => setTouchedFields(prevTouched => ({ ...prevTouched, name: true }))}
|
||||
data-test-subj="nameInput"
|
||||
onChange={e => {
|
||||
updateTemplate({ name: e.target.value });
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
<UseField
|
||||
path="name"
|
||||
component={FormRow}
|
||||
componentProps={{
|
||||
title: i18n.name.title,
|
||||
titleTag: 'h3',
|
||||
description: i18n.name.description,
|
||||
idAria: 'stepLogisticsNameDescription',
|
||||
euiFieldProps: { disabled: isEditing },
|
||||
['data-test-subj']: 'nameField',
|
||||
}}
|
||||
/>
|
||||
{/* Index patterns */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle"
|
||||
defaultMessage="Index patterns"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription"
|
||||
defaultMessage="The index patterns to apply to the template."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsIndexPatternsDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsLabel"
|
||||
defaultMessage="Index patterns"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsHelpText"
|
||||
defaultMessage="Spaces and the characters {invalidCharactersList} are not allowed."
|
||||
values={{
|
||||
invalidCharactersList: <strong>{indexPatternInvalidCharacters}</strong>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={isIndexPatternsTouched && Boolean(indexPatternsError)}
|
||||
error={indexPatternsError}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
noSuggestions
|
||||
fullWidth
|
||||
data-test-subj="indexPatternsComboBox"
|
||||
selectedOptions={indexPatternOptions}
|
||||
onBlur={() =>
|
||||
setTouchedFields(prevTouched => ({ ...prevTouched, indexPatterns: true }))
|
||||
}
|
||||
onChange={(selectedPattern: EuiComboBoxOptionProps[]) => {
|
||||
const newIndexPatterns = selectedPattern.map(({ value }) => value as string);
|
||||
updateTemplate({ indexPatterns: newIndexPatterns });
|
||||
}}
|
||||
onCreateOption={(selectedPattern: string) => {
|
||||
if (!selectedPattern.trim().length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndexPatterns = [...indexPatterns, selectedPattern];
|
||||
|
||||
setAllIndexPatterns([...allIndexPatterns, selectedPattern]);
|
||||
updateTemplate({ indexPatterns: newIndexPatterns });
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
<UseField
|
||||
path="indexPatterns"
|
||||
component={FormRow}
|
||||
componentProps={{
|
||||
title: i18n.indexPatterns.title,
|
||||
titleTag: 'h3',
|
||||
description: i18n.indexPatterns.description,
|
||||
idAria: 'stepLogisticsIndexPatternsDescription',
|
||||
['data-test-subj']: 'indexPatternsField',
|
||||
}}
|
||||
/>
|
||||
{/* Order */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.orderTitle"
|
||||
defaultMessage="Merge order"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.orderDescription"
|
||||
defaultMessage="The merge order when multiple templates match an index."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsOrderDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.fieldOrderLabel"
|
||||
defaultMessage="Order (optional)"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
value={order}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
updateTemplate({ order: value === '' ? value : Number(value) });
|
||||
}}
|
||||
data-test-subj="orderInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>{' '}
|
||||
<UseField
|
||||
path="order"
|
||||
component={FormRow}
|
||||
componentProps={{
|
||||
title: i18n.order.title,
|
||||
titleTag: 'h3',
|
||||
description: i18n.order.description,
|
||||
idAria: 'stepLogisticsOrderDescription',
|
||||
['data-test-subj']: 'orderField',
|
||||
}}
|
||||
/>
|
||||
{/* Version */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.versionTitle"
|
||||
defaultMessage="Version"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.versionDescription"
|
||||
defaultMessage="A number that identifies the template to external management systems."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsVersionDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.fieldVersionLabel"
|
||||
defaultMessage="Version (optional)"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
value={version}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
updateTemplate({ version: value === '' ? value : Number(value) });
|
||||
}}
|
||||
data-test-subj="versionInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</div>
|
||||
<UseField
|
||||
path="version"
|
||||
component={FormRow}
|
||||
componentProps={{
|
||||
title: i18n.version.title,
|
||||
titleTag: 'h3',
|
||||
description: i18n.version.description,
|
||||
idAria: 'stepLogisticsVersionDescription',
|
||||
euiFieldProps: { ['data-test-subj']: 'versionField' },
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,14 +20,19 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { mappingDocumentationLink } from '../../../lib/documentation_links';
|
||||
import { StepProps } from '../types';
|
||||
import { useJsonStep } from './use_json_step';
|
||||
|
||||
export const StepMappings: React.FunctionComponent<StepProps> = ({
|
||||
template,
|
||||
updateTemplate,
|
||||
errors,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
}) => {
|
||||
const { mappings } = template;
|
||||
const { mappings: mappingsError } = errors;
|
||||
const { content, setContent, error } = useJsonStep({
|
||||
prop: 'mappings',
|
||||
defaultValue: template.mappings,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-test-subj="stepMappings">
|
||||
|
@ -97,8 +102,8 @@ export const StepMappings: React.FunctionComponent<StepProps> = ({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(mappingsError)}
|
||||
error={mappingsError}
|
||||
isInvalid={Boolean(error)}
|
||||
error={error}
|
||||
fullWidth
|
||||
>
|
||||
<EuiCodeEditor
|
||||
|
@ -121,9 +126,9 @@ export const StepMappings: React.FunctionComponent<StepProps> = ({
|
|||
defaultMessage: 'Mappings editor',
|
||||
}
|
||||
)}
|
||||
value={mappings}
|
||||
onChange={(newMappings: string) => {
|
||||
updateTemplate({ mappings: newMappings });
|
||||
value={content}
|
||||
onChange={(udpated: string) => {
|
||||
setContent(udpated);
|
||||
}}
|
||||
data-test-subj="mappingsEditor"
|
||||
/>
|
||||
|
|
|
@ -20,9 +20,14 @@ import {
|
|||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { serializers } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
|
||||
|
||||
import { serializeTemplate } from '../../../../common/lib/template_serialization';
|
||||
import { Template } from '../../../../common/types';
|
||||
import { StepProps } from '../types';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
|
||||
const NoneDescriptionText = () => (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepReview.summaryTab.noneDescriptionText"
|
||||
|
@ -49,7 +54,7 @@ const getDescriptionText = (data: any) => {
|
|||
export const StepReview: React.FunctionComponent<StepProps> = ({ template, updateCurrentStep }) => {
|
||||
const { name, indexPatterns, version, order } = template;
|
||||
|
||||
const serializedTemplate = serializeTemplate(template);
|
||||
const serializedTemplate = serializeTemplate(stripEmptyFields(template) as Template);
|
||||
// Name not included in ES request body
|
||||
delete serializedTemplate.name;
|
||||
const {
|
||||
|
@ -58,9 +63,9 @@ export const StepReview: React.FunctionComponent<StepProps> = ({ template, updat
|
|||
aliases: serializedAliases,
|
||||
} = serializedTemplate;
|
||||
|
||||
const numIndexPatterns = indexPatterns.length;
|
||||
const numIndexPatterns = indexPatterns!.length;
|
||||
|
||||
const hasWildCardIndexPattern = Boolean(indexPatterns.find(pattern => pattern === '*'));
|
||||
const hasWildCardIndexPattern = Boolean(indexPatterns!.find(pattern => pattern === '*'));
|
||||
|
||||
const SummaryTab = () => (
|
||||
<div data-test-subj="summaryTab">
|
||||
|
@ -80,7 +85,7 @@ export const StepReview: React.FunctionComponent<StepProps> = ({ template, updat
|
|||
{numIndexPatterns > 1 ? (
|
||||
<EuiText>
|
||||
<ul>
|
||||
{indexPatterns.map((indexName: string, i: number) => {
|
||||
{indexPatterns!.map((indexName: string, i: number) => {
|
||||
return (
|
||||
<li key={`${indexName}-${i}`}>
|
||||
<EuiTitle size="xs">
|
||||
|
@ -92,7 +97,7 @@ export const StepReview: React.FunctionComponent<StepProps> = ({ template, updat
|
|||
</ul>
|
||||
</EuiText>
|
||||
) : (
|
||||
indexPatterns.toString()
|
||||
indexPatterns!.toString()
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
|
||||
|
|
|
@ -20,14 +20,19 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { settingsDocumentationLink } from '../../../lib/documentation_links';
|
||||
import { StepProps } from '../types';
|
||||
import { useJsonStep } from './use_json_step';
|
||||
|
||||
export const StepSettings: React.FunctionComponent<StepProps> = ({
|
||||
template,
|
||||
updateTemplate,
|
||||
errors,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
}) => {
|
||||
const { settings } = template;
|
||||
const { settings: settingsError } = errors;
|
||||
const { content, setContent, error } = useJsonStep({
|
||||
prop: 'settings',
|
||||
defaultValue: template.settings,
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-test-subj="stepSettings">
|
||||
|
@ -89,8 +94,8 @@ export const StepSettings: React.FunctionComponent<StepProps> = ({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(settingsError)}
|
||||
error={settingsError}
|
||||
isInvalid={Boolean(error)}
|
||||
error={error}
|
||||
fullWidth
|
||||
>
|
||||
<EuiCodeEditor
|
||||
|
@ -113,10 +118,8 @@ export const StepSettings: React.FunctionComponent<StepProps> = ({
|
|||
defaultMessage: 'Index settings editor',
|
||||
}
|
||||
)}
|
||||
value={settings}
|
||||
onChange={(newSettings: string) => {
|
||||
updateTemplate({ settings: newSettings });
|
||||
}}
|
||||
value={content}
|
||||
onChange={(updated: string) => setContent(updated)}
|
||||
data-test-subj="settingsEditor"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { isJSON } from '../../../../../../../../src/plugins/es_ui_shared/static/validators/string';
|
||||
import { StepProps } from '../types';
|
||||
|
||||
interface Parameters {
|
||||
prop: 'settings' | 'mappings' | 'aliases';
|
||||
setDataGetter: StepProps['setDataGetter'];
|
||||
onStepValidityChange: StepProps['onStepValidityChange'];
|
||||
defaultValue?: object;
|
||||
}
|
||||
|
||||
const stringifyJson = (json: any) =>
|
||||
Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}';
|
||||
|
||||
export const useJsonStep = ({
|
||||
prop,
|
||||
defaultValue = {},
|
||||
setDataGetter,
|
||||
onStepValidityChange,
|
||||
}: Parameters) => {
|
||||
const [content, setContent] = useState<string>(stringifyJson(defaultValue));
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateContent = () => {
|
||||
// We allow empty string as it will be converted to "{}""
|
||||
const isValid = content.trim() === '' ? true : isJSON(content);
|
||||
if (!isValid) {
|
||||
setError(
|
||||
i18n.translate('xpack.idxMgmt.validators.string.invalidJSONError', {
|
||||
defaultMessage: 'Invalid JSON format.',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setError(null);
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const dataGetter = () => {
|
||||
const isValid = validateContent();
|
||||
const value = isValid && content.trim() !== '' ? JSON.parse(content) : {};
|
||||
const data = { [prop]: value };
|
||||
return Promise.resolve({ isValid, data });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isValid = validateContent();
|
||||
onStepValidityChange(isValid);
|
||||
setDataGetter(dataGetter);
|
||||
}, [content]);
|
||||
|
||||
return {
|
||||
content,
|
||||
setContent,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import React, { Fragment, useState, useRef } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -13,32 +13,30 @@ import {
|
|||
EuiForm,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { serializers } from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
|
||||
import { Template } from '../../../common/types';
|
||||
import { TemplateSteps } from './template_steps';
|
||||
import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps';
|
||||
import {
|
||||
validateLogistics,
|
||||
validateSettings,
|
||||
validateMappings,
|
||||
validateAliases,
|
||||
TemplateValidation,
|
||||
} from '../../services/template_validation';
|
||||
import { StepProps } from './types';
|
||||
import { SectionError } from '..';
|
||||
import { StepProps, DataGetterFunc } from './types';
|
||||
import { SectionError } from '../section_error';
|
||||
|
||||
const { stripEmptyFields } = serializers;
|
||||
|
||||
interface Props {
|
||||
onSave: (template: Template) => void;
|
||||
clearSaveError: () => void;
|
||||
isSaving: boolean;
|
||||
saveError: any;
|
||||
template: Template;
|
||||
defaultValue?: Template;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
const defaultValidation = {
|
||||
isValid: true,
|
||||
errors: {},
|
||||
};
|
||||
interface ValidationState {
|
||||
[key: number]: { isValid: boolean };
|
||||
}
|
||||
|
||||
const defaultValidation = { isValid: true };
|
||||
|
||||
const stepComponentMap: { [key: number]: React.FunctionComponent<StepProps> } = {
|
||||
1: StepLogistics,
|
||||
|
@ -48,25 +46,16 @@ const stepComponentMap: { [key: number]: React.FunctionComponent<StepProps> } =
|
|||
5: StepReview,
|
||||
};
|
||||
|
||||
const stepValidationMap: { [key: number]: any } = {
|
||||
1: validateLogistics,
|
||||
2: validateSettings,
|
||||
3: validateMappings,
|
||||
4: validateAliases,
|
||||
};
|
||||
|
||||
export const TemplateForm: React.FunctionComponent<Props> = ({
|
||||
template: initialTemplate,
|
||||
defaultValue = { isManaged: false },
|
||||
onSave,
|
||||
isSaving,
|
||||
saveError,
|
||||
clearSaveError,
|
||||
isEditing,
|
||||
}) => {
|
||||
// hooks
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [template, setTemplate] = useState<Template>(initialTemplate);
|
||||
const [validation, setValidation] = useState<{ [key: number]: TemplateValidation }>({
|
||||
const [validation, setValidation] = useState<ValidationState>({
|
||||
1: defaultValidation,
|
||||
2: defaultValidation,
|
||||
3: defaultValidation,
|
||||
|
@ -74,42 +63,56 @@ export const TemplateForm: React.FunctionComponent<Props> = ({
|
|||
5: defaultValidation,
|
||||
});
|
||||
|
||||
const template = useRef<Partial<Template>>(defaultValue);
|
||||
const stepsDataGetters = useRef<Record<number, DataGetterFunc>>({});
|
||||
|
||||
const lastStep = Object.keys(stepComponentMap).length;
|
||||
|
||||
const CurrentStepComponent = stepComponentMap[currentStep];
|
||||
|
||||
const validateStep = stepValidationMap[currentStep];
|
||||
|
||||
const stepErrors = validation[currentStep].errors;
|
||||
const isStepValid = validation[currentStep].isValid;
|
||||
|
||||
const updateValidation = (templateToValidate: Template): void => {
|
||||
const stepValidation = validateStep(templateToValidate);
|
||||
const setStepDataGetter = (stepDataGetter: DataGetterFunc) => {
|
||||
stepsDataGetters.current[currentStep] = stepDataGetter;
|
||||
};
|
||||
|
||||
const newValidation = {
|
||||
...validation,
|
||||
...{
|
||||
[currentStep]: stepValidation,
|
||||
const onStepValidityChange = (isValid: boolean) => {
|
||||
setValidation(prev => ({
|
||||
...prev,
|
||||
[currentStep]: {
|
||||
isValid,
|
||||
errors: {},
|
||||
},
|
||||
};
|
||||
|
||||
setValidation(newValidation);
|
||||
}));
|
||||
};
|
||||
|
||||
const updateTemplate = (updatedTemplate: Partial<Template>): void => {
|
||||
const newTemplate = { ...template, ...updatedTemplate };
|
||||
const validateAndGetDataFromCurrentStep = async () => {
|
||||
const validateAndGetData = stepsDataGetters.current[currentStep];
|
||||
|
||||
updateValidation(newTemplate);
|
||||
setTemplate(newTemplate);
|
||||
if (!validateAndGetData) {
|
||||
throw new Error(`No data getter has been set for step "${currentStep}"`);
|
||||
}
|
||||
|
||||
const { isValid, data } = await validateAndGetData();
|
||||
|
||||
if (isValid) {
|
||||
// Update the template object
|
||||
template.current = { ...template.current, ...data };
|
||||
}
|
||||
|
||||
return { isValid, data };
|
||||
};
|
||||
|
||||
const updateCurrentStep = (nextStep: number) => {
|
||||
const updateCurrentStep = async (nextStep: number) => {
|
||||
// All steps needs validation, except for the last step
|
||||
const shouldValidate = currentStep !== lastStep;
|
||||
|
||||
// If step is invalid do not let user proceed
|
||||
if (shouldValidate && !isStepValid) {
|
||||
return;
|
||||
let isValid = isStepValid;
|
||||
if (shouldValidate) {
|
||||
isValid = isValid === false ? false : (await validateAndGetDataFromCurrentStep()).isValid;
|
||||
|
||||
// If step is invalid do not let user proceed
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentStep(nextStep);
|
||||
|
@ -138,12 +141,6 @@ export const TemplateForm: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
updateValidation(template);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TemplateSteps
|
||||
|
@ -172,10 +169,10 @@ export const TemplateForm: React.FunctionComponent<Props> = ({
|
|||
|
||||
<EuiForm data-test-subj="templateForm">
|
||||
<CurrentStepComponent
|
||||
template={template}
|
||||
updateTemplate={updateTemplate}
|
||||
errors={stepErrors}
|
||||
template={template.current}
|
||||
setDataGetter={setStepDataGetter}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
onStepValidityChange={onStepValidityChange}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
@ -218,7 +215,7 @@ export const TemplateForm: React.FunctionComponent<Props> = ({
|
|||
fill
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={onSave.bind(null, template)}
|
||||
onClick={onSave.bind(null, stripEmptyFields(template.current) as Template)}
|
||||
data-test-subj="submitButton"
|
||||
isLoading={isSaving}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
FormSchema,
|
||||
FIELD_TYPES,
|
||||
VALIDATION_TYPES,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
|
||||
import {
|
||||
fieldFormatters,
|
||||
fieldValidators,
|
||||
} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers';
|
||||
|
||||
import {
|
||||
INVALID_INDEX_PATTERN_CHARS,
|
||||
INVALID_TEMPLATE_NAME_CHARS,
|
||||
} from '../../../common/constants';
|
||||
|
||||
const { emptyField, containsCharsField, startsWithField, indexPatternField } = fieldValidators;
|
||||
const { toInt } = fieldFormatters;
|
||||
const indexPatternInvalidCharacters = INVALID_INDEX_PATTERN_CHARS.join(' ');
|
||||
|
||||
export const schemas: Record<string, FormSchema> = {
|
||||
logistics: {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldNameLabel', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.templateNameRequiredError', {
|
||||
defaultMessage: 'A template name is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: containsCharsField({
|
||||
chars: ' ',
|
||||
message: i18n.translate('xpack.idxMgmt.templateValidation.templateNameSpacesError', {
|
||||
defaultMessage: 'Spaces are not allowed in a template name.',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
validator: startsWithField({
|
||||
char: '_',
|
||||
message: i18n.translate(
|
||||
'xpack.idxMgmt.templateValidation.templateNameUnderscoreError',
|
||||
{
|
||||
defaultMessage: 'A template name must not start with an underscore.',
|
||||
}
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
validator: startsWithField({
|
||||
char: '.',
|
||||
message: i18n.translate('xpack.idxMgmt.templateValidation.templateNamePeriodError', {
|
||||
defaultMessage: 'A template name must not start with a period.',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
validator: containsCharsField({
|
||||
chars: INVALID_TEMPLATE_NAME_CHARS,
|
||||
message: ({ charsFound }) =>
|
||||
i18n.translate(
|
||||
'xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError',
|
||||
{
|
||||
defaultMessage: 'A template name must not contain the character "{invalidChar}"',
|
||||
values: { invalidChar: charsFound[0] },
|
||||
}
|
||||
),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
indexPatterns: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
defaultValue: [],
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsLabel', {
|
||||
defaultMessage: 'Index patterns',
|
||||
}),
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsHelpText"
|
||||
defaultMessage="Spaces and the characters {invalidCharactersList} are not allowed."
|
||||
values={{
|
||||
invalidCharactersList: <strong>{indexPatternInvalidCharacters}</strong>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.indexPatternsRequiredError', {
|
||||
defaultMessage: 'At least one index pattern is required.',
|
||||
})
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: indexPatternField(i18n),
|
||||
type: VALIDATION_TYPES.ARRAY_ITEM,
|
||||
},
|
||||
],
|
||||
},
|
||||
order: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldOrderLabel', {
|
||||
defaultMessage: 'Order (optional)',
|
||||
}),
|
||||
formatters: [toInt],
|
||||
},
|
||||
version: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldVersionLabel', {
|
||||
defaultMessage: 'Version (optional)',
|
||||
}),
|
||||
formatters: [toInt],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -5,12 +5,13 @@
|
|||
*/
|
||||
|
||||
import { Template } from '../../../common/types';
|
||||
import { TemplateValidation } from '../../services/template_validation';
|
||||
|
||||
export interface StepProps {
|
||||
template: Template;
|
||||
updateTemplate: (updatedTemplate: Partial<Template>) => void;
|
||||
template: Partial<Template>;
|
||||
setDataGetter: (dataGetter: DataGetterFunc) => void;
|
||||
updateCurrentStep: (step: number) => void;
|
||||
errors: TemplateValidation['errors'];
|
||||
onStepValidityChange: (isValid: boolean) => void;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export type DataGetterFunc = () => Promise<{ isValid: boolean; data: any }>;
|
||||
|
|
|
@ -16,10 +16,10 @@ interface Props {
|
|||
export const TabAliases: React.FunctionComponent<Props> = ({ templateDetails }) => {
|
||||
const { aliases } = templateDetails;
|
||||
|
||||
if (aliases) {
|
||||
if (aliases && Object.keys(aliases).length) {
|
||||
return (
|
||||
<div data-test-subj="aliasesTab">
|
||||
<EuiCodeBlock lang="json">{aliases}</EuiCodeBlock>
|
||||
<EuiCodeBlock lang="json">{JSON.stringify(aliases, null, 2)}</EuiCodeBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@ interface Props {
|
|||
export const TabMappings: React.FunctionComponent<Props> = ({ templateDetails }) => {
|
||||
const { mappings } = templateDetails;
|
||||
|
||||
if (mappings) {
|
||||
if (mappings && Object.keys(mappings).length) {
|
||||
return (
|
||||
<div data-test-subj="mappingsTab">
|
||||
<EuiCodeBlock lang="json">{mappings}</EuiCodeBlock>
|
||||
<EuiCodeBlock lang="json">{JSON.stringify(mappings, null, 2)}</EuiCodeBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@ interface Props {
|
|||
export const TabSettings: React.FunctionComponent<Props> = ({ templateDetails }) => {
|
||||
const { settings } = templateDetails;
|
||||
|
||||
if (settings) {
|
||||
if (settings && Object.keys(settings).length) {
|
||||
return (
|
||||
<div data-test-subj="settingsTab">
|
||||
<EuiCodeBlock lang="json">{settings}</EuiCodeBlock>
|
||||
<EuiCodeBlock lang="json">{JSON.stringify(settings, null, 2)}</EuiCodeBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -84,12 +84,12 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar
|
|||
} else if (templateToClone) {
|
||||
const templateData = {
|
||||
...templateToClone,
|
||||
...{ name: `${decodedTemplateName}-copy` },
|
||||
name: `${decodedTemplateName}-copy`,
|
||||
} as Template;
|
||||
|
||||
content = (
|
||||
<TemplateForm
|
||||
template={templateData}
|
||||
defaultValue={templateData}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
|
|
|
@ -13,19 +13,6 @@ import { Template } from '../../../common/types';
|
|||
import { saveTemplate } from '../../services/api';
|
||||
import { getTemplateDetailsLink } from '../../services/routing';
|
||||
|
||||
const emptyObject = '{\n\n}';
|
||||
|
||||
const DEFAULT_TEMPLATE: Template = {
|
||||
name: '',
|
||||
indexPatterns: [],
|
||||
version: '',
|
||||
order: '',
|
||||
settings: emptyObject,
|
||||
mappings: emptyObject,
|
||||
aliases: emptyObject,
|
||||
isManaged: false,
|
||||
};
|
||||
|
||||
export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
@ -69,7 +56,6 @@ export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ h
|
|||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
<TemplateForm
|
||||
template={DEFAULT_TEMPLATE}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
|
|
|
@ -125,7 +125,7 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</Fragment>
|
||||
)}
|
||||
<TemplateForm
|
||||
template={template}
|
||||
defaultValue={template}
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { removeEmptyErrorFields, isValid, isStringEmpty } from './validation_helpers';
|
||||
export { validateLogistics } from './validation_logistics';
|
||||
export { validateSettings } from './validation_settings';
|
||||
export { validateMappings } from './validation_mappings';
|
||||
export { validateAliases } from './validation_aliases';
|
||||
export { TemplateValidation } from './types';
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface TemplateValidation {
|
||||
isValid: boolean;
|
||||
errors: { [key: string]: React.ReactNode[] };
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 { Template } from '../../../common/types';
|
||||
import { TemplateValidation } from './types';
|
||||
import { isStringEmpty, removeEmptyErrorFields, isValid, validateJSON } from './validation_helpers';
|
||||
|
||||
export const validateAliases = (template: Template): TemplateValidation => {
|
||||
const { aliases } = template;
|
||||
|
||||
const validation: TemplateValidation = {
|
||||
isValid: true,
|
||||
errors: {
|
||||
aliases: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Aliases JSON validation
|
||||
if (typeof aliases === 'string' && !isStringEmpty(aliases)) {
|
||||
const validationMsg = validateJSON(aliases);
|
||||
|
||||
if (typeof validationMsg === 'string') {
|
||||
validation.errors.aliases.push(validationMsg);
|
||||
}
|
||||
}
|
||||
|
||||
validation.errors = removeEmptyErrorFields(validation.errors);
|
||||
validation.isValid = isValid(validation.errors);
|
||||
|
||||
return validation;
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { TemplateValidation } from './types';
|
||||
|
||||
export const removeEmptyErrorFields = (errors: TemplateValidation['errors']) => {
|
||||
return Object.entries(errors)
|
||||
.filter(([_key, value]) => value.length > 0)
|
||||
.reduce((errs: TemplateValidation['errors'], [key, value]) => {
|
||||
errs[key] = value;
|
||||
return errs;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const isValid = (errors: TemplateValidation['errors']) => {
|
||||
return Boolean(Object.keys(errors).length === 0);
|
||||
};
|
||||
|
||||
export const isStringEmpty = (str: string | null): boolean => {
|
||||
return str ? !Boolean(str.trim()) : true;
|
||||
};
|
||||
|
||||
export const validateJSON = (jsonString: string) => {
|
||||
const invalidJsonMsg = i18n.translate('xpack.idxMgmt.templateValidation.invalidJSONError', {
|
||||
defaultMessage: 'Invalid JSON format.',
|
||||
});
|
||||
|
||||
try {
|
||||
const parsedSettingsJson = JSON.parse(jsonString);
|
||||
if (parsedSettingsJson && typeof parsedSettingsJson !== 'object') {
|
||||
return invalidJsonMsg;
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
return invalidJsonMsg;
|
||||
}
|
||||
};
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ILLEGAL_CHARACTERS,
|
||||
CONTAINS_SPACES,
|
||||
validateIndexPattern as getIndexPatternErrors,
|
||||
} from 'ui/index_patterns';
|
||||
import { Template } from '../../../common/types';
|
||||
import { TemplateValidation } from './types';
|
||||
import { isStringEmpty, removeEmptyErrorFields, isValid } from './validation_helpers';
|
||||
|
||||
const validateIndexPattern = (indexPattern: string) => {
|
||||
if (indexPattern) {
|
||||
const errors = getIndexPatternErrors(indexPattern);
|
||||
|
||||
if (errors[ILLEGAL_CHARACTERS]) {
|
||||
return i18n.translate('xpack.idxMgmt.templateValidation.indexPatternInvalidCharactersError', {
|
||||
defaultMessage:
|
||||
"'{indexPattern}' index pattern contains the invalid {characterListLength, plural, one {character} other {characters}} { characterList }.",
|
||||
values: {
|
||||
characterList: errors[ILLEGAL_CHARACTERS].join(' '),
|
||||
characterListLength: errors[ILLEGAL_CHARACTERS].length,
|
||||
indexPattern,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (errors[CONTAINS_SPACES]) {
|
||||
return i18n.translate('xpack.idxMgmt.templateValidation.indexPatternSpacesError', {
|
||||
defaultMessage: "'{indexPattern}' index pattern contains spaces.",
|
||||
values: {
|
||||
indexPattern,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const INVALID_NAME_CHARS = ['"', '*', '\\', ',', '?'];
|
||||
|
||||
const doesStringIncludeChar = (string: string, chars: string[]) => {
|
||||
const invalidChar = chars.find(char => string.includes(char)) || null;
|
||||
const containsChar = invalidChar !== null;
|
||||
|
||||
return { containsChar, invalidChar };
|
||||
};
|
||||
|
||||
export const validateLogistics = (template: Template): TemplateValidation => {
|
||||
const { name, indexPatterns } = template;
|
||||
|
||||
const validation: TemplateValidation = {
|
||||
isValid: true,
|
||||
errors: {
|
||||
indexPatterns: [],
|
||||
name: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Name validation
|
||||
if (name !== undefined && isStringEmpty(name)) {
|
||||
validation.errors.name.push(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.templateNameRequiredError', {
|
||||
defaultMessage: 'A template name is required.',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (name.includes(' ')) {
|
||||
validation.errors.name.push(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.templateNameSpacesError', {
|
||||
defaultMessage: 'Spaces are not allowed in a template name.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (name.startsWith('_')) {
|
||||
validation.errors.name.push(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.templateNameUnderscoreError', {
|
||||
defaultMessage: 'A template name must not start with an underscore.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (name.startsWith('.')) {
|
||||
validation.errors.name.push(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.templateNamePeriodError', {
|
||||
defaultMessage: 'A template name must not start with a period.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { containsChar, invalidChar } = doesStringIncludeChar(name, INVALID_NAME_CHARS);
|
||||
|
||||
if (containsChar) {
|
||||
validation.errors.name = [
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError', {
|
||||
defaultMessage: 'A template name must not contain the character "{invalidChar}"',
|
||||
values: { invalidChar },
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Index patterns validation
|
||||
if (Array.isArray(indexPatterns) && indexPatterns.length === 0) {
|
||||
validation.errors.indexPatterns.push(
|
||||
i18n.translate('xpack.idxMgmt.templateValidation.indexPatternsRequiredError', {
|
||||
defaultMessage: 'At least one index pattern is required.',
|
||||
})
|
||||
);
|
||||
} else if (Array.isArray(indexPatterns) && indexPatterns.length) {
|
||||
indexPatterns.forEach(pattern => {
|
||||
const errorMsg = validateIndexPattern(pattern);
|
||||
if (errorMsg) {
|
||||
validation.errors.indexPatterns.push(errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validation.errors = removeEmptyErrorFields(validation.errors);
|
||||
validation.isValid = isValid(validation.errors);
|
||||
|
||||
return validation;
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 { Template } from '../../../common/types';
|
||||
import { TemplateValidation } from './types';
|
||||
import { isStringEmpty, removeEmptyErrorFields, isValid, validateJSON } from './validation_helpers';
|
||||
|
||||
export const validateMappings = (template: Template): TemplateValidation => {
|
||||
const { mappings } = template;
|
||||
|
||||
const validation: TemplateValidation = {
|
||||
isValid: true,
|
||||
errors: {
|
||||
mappings: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Mappings JSON validation
|
||||
if (typeof mappings === 'string' && !isStringEmpty(mappings)) {
|
||||
const validationMsg = validateJSON(mappings);
|
||||
|
||||
if (typeof validationMsg === 'string') {
|
||||
validation.errors.mappings.push(validationMsg);
|
||||
}
|
||||
}
|
||||
|
||||
validation.errors = removeEmptyErrorFields(validation.errors);
|
||||
validation.isValid = isValid(validation.errors);
|
||||
|
||||
return validation;
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 { Template } from '../../../common/types';
|
||||
import { TemplateValidation } from './types';
|
||||
import { isStringEmpty, removeEmptyErrorFields, isValid, validateJSON } from './validation_helpers';
|
||||
|
||||
export const validateSettings = (template: Template): TemplateValidation => {
|
||||
const { settings } = template;
|
||||
|
||||
const validation: TemplateValidation = {
|
||||
isValid: true,
|
||||
errors: {
|
||||
settings: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Settings JSON validation
|
||||
if (typeof settings === 'string' && !isStringEmpty(settings)) {
|
||||
const validationMsg = validateJSON(settings);
|
||||
|
||||
if (typeof validationMsg === 'string') {
|
||||
validation.errors.settings.push(validationMsg);
|
||||
}
|
||||
}
|
||||
|
||||
validation.errors = removeEmptyErrorFields(validation.errors);
|
||||
validation.isValid = isValid(validation.errors);
|
||||
|
||||
return validation;
|
||||
};
|
|
@ -4916,10 +4916,7 @@
|
|||
"xpack.idxMgmt.templateList.table.orderColumnTitle": "順序",
|
||||
"xpack.idxMgmt.templateList.table.reloadTemplatesButtonLabel": "再読み込み",
|
||||
"xpack.idxMgmt.templateList.table.settingsColumnTitle": "設定",
|
||||
"xpack.idxMgmt.templateValidation.indexPatternInvalidCharactersError": "「{indexPattern}」インデックスパターンには無効な{characterListLength, plural, one {文字} other {文字}} { characterList } が含まれています。",
|
||||
"xpack.idxMgmt.templateValidation.indexPatternSpacesError": "「{indexPattern}」インデックスパターンにはスペースが含まれています。",
|
||||
"xpack.idxMgmt.templateValidation.indexPatternsRequiredError": "インデックスパターンが最低 1 つ必要です。",
|
||||
"xpack.idxMgmt.templateValidation.invalidJSONError": "無効な JSON フォーマット。",
|
||||
"xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError": "テンプレート名に「{invalidChar}」は使用できません",
|
||||
"xpack.idxMgmt.templateValidation.templateNamePeriodError": "テンプレート名はピリオドで始めることはできません。",
|
||||
"xpack.idxMgmt.templateValidation.templateNameRequiredError": "テンプレート名が必要です。",
|
||||
|
|
|
@ -4919,10 +4919,7 @@
|
|||
"xpack.idxMgmt.templateList.table.orderColumnTitle": "顺序",
|
||||
"xpack.idxMgmt.templateList.table.reloadTemplatesButtonLabel": "重新加载",
|
||||
"xpack.idxMgmt.templateList.table.settingsColumnTitle": "设置",
|
||||
"xpack.idxMgmt.templateValidation.indexPatternInvalidCharactersError": "“{indexPattern}”索引模式包含无效的{characterListLength, plural, one {字符} other {字符}} { characterList }。",
|
||||
"xpack.idxMgmt.templateValidation.indexPatternSpacesError": "“{indexPattern}”索引模式包含空格。",
|
||||
"xpack.idxMgmt.templateValidation.indexPatternsRequiredError": "至少需要一个索引模式。",
|
||||
"xpack.idxMgmt.templateValidation.invalidJSONError": "JSON 格式无效。",
|
||||
"xpack.idxMgmt.templateValidation.templateNameInvalidaCharacterError": "模板名称不得包含字符“{invalidChar}”",
|
||||
"xpack.idxMgmt.templateValidation.templateNamePeriodError": "模板名称不得以句点开头。",
|
||||
"xpack.idxMgmt.templateValidation.templateNameRequiredError": "模板名称必填。",
|
||||
|
|
|
@ -16,15 +16,15 @@ export const registerHelpers = ({ supertest }) => {
|
|||
order: 1,
|
||||
indexPatterns: INDEX_PATTERNS,
|
||||
version: 1,
|
||||
settings: JSON.stringify({
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
index: {
|
||||
lifecycle: {
|
||||
name: 'my_policy',
|
||||
}
|
||||
}
|
||||
}),
|
||||
mappings: JSON.stringify({
|
||||
},
|
||||
mappings: {
|
||||
_source: {
|
||||
enabled: false
|
||||
},
|
||||
|
@ -37,10 +37,10 @@ export const registerHelpers = ({ supertest }) => {
|
|||
format: 'EEE MMM dd HH:mm:ss Z yyyy'
|
||||
}
|
||||
}
|
||||
}),
|
||||
aliases: JSON.stringify({
|
||||
},
|
||||
aliases: {
|
||||
alias1: {}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const createTemplate = payload =>
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
*/
|
||||
|
||||
export { registerTestBed } from './testbed';
|
||||
export { TestBed, TestBedConfig } from './types';
|
||||
export { TestBed, TestBedConfig, SetupFunc } from './types';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue