From bcf377ad011760b0d04ccc3e72a42a1ff395f5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 9 Jul 2019 16:55:30 +0200 Subject: [PATCH 01/64] Move mappings editor component to its own folder --- .../static/ui/components/mappings_editor/index.ts | 7 +++++++ .../components/{ => mappings_editor}/mappings_editor.tsx | 0 2 files changed, 7 insertions(+) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts rename x-pack/legacy/plugins/index_management/static/ui/components/{ => mappings_editor}/mappings_editor.tsx (100%) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts new file mode 100644 index 0000000000000..47c0a71408a7c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './mappings_editor'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor.tsx rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx From f2792018351ce9943042310822fc8959a1737232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 11 Jul 2019 10:17:25 +0200 Subject: [PATCH 02/64] Initial version of the mappings editor --- .../mappings_editor/components/index.ts | 7 + .../components/mappings_property.tsx | 88 ++++++++++++ .../components/property_common_parameters.tsx | 42 ++++++ .../mappings_editor/data_types_config.ts | 38 +++++ .../components/mappings_editor/form.schema.ts | 45 ++++++ .../ui/components/mappings_editor/helpers.ts | 12 ++ .../mappings_editor/mappings_editor.tsx | 136 +++++++++--------- .../components/mappings_editor/parameters.ts | 70 +++++++++ 8 files changed, 372 insertions(+), 66 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts new file mode 100644 index 0000000000000..91f38eb171a06 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './mappings_property'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx new file mode 100644 index 0000000000000..f7e30315c7a82 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx @@ -0,0 +1,88 @@ +/* + * 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, { Fragment, useState } from 'react'; +import { + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiButtonIcon, + EuiFormRow, +} from '@elastic/eui'; +import { + UseField, + Form, +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; + +import { parameters } from '../parameters'; +import { dataTypesConfig, DataTypeConfig } from '../data_types_config'; +import { PropertyCommonParameters } from './property_common_parameters'; + +interface Props { + form: Form; + onRemove: () => void; + fieldPathPrefix?: string; +} + +export const MappingsProperty = ({ onRemove, fieldPathPrefix = '', form }: Props) => { + const [propertyType, setPropertyType] = useState( + dataTypesConfig.find(config => config.value === 'text')! + ); + + return ( + + + + + + + + {field => ( + + { + setPropertyType(dataTypesConfig.find(config => config.value === value)!); + field.setValue(value); + }} + hasNoInitialSelection={true} + isInvalid={false} + options={dataTypesConfig.map(({ value, text }) => ({ value, text }))} + /> + + )} + + + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx new file mode 100644 index 0000000000000..ac8c0476bf5e9 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx @@ -0,0 +1,42 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + UseField, + Form, +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +import { DataTypeConfig } from '../data_types_config'; +import { parameters } from '../parameters'; + +interface Props { + form: Form; + typeConfig: DataTypeConfig | null; + fieldPathPrefix?: string; +} + +export const PropertyCommonParameters = ({ form, typeConfig, fieldPathPrefix = '' }: Props) => { + if (!typeConfig || !typeConfig.commonParameters) { + return null; + } + return ( + + {typeConfig.commonParameters.map(parameter => ( + + + + ))} + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts new file mode 100644 index 0000000000000..efbb0718e3c3c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts @@ -0,0 +1,38 @@ +/* + * 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 { ParameterName } from './parameters'; + +export type DataType = 'text' | 'keyword' | 'object' | 'array' | 'boolean'; + +export interface DataTypeConfig { + value: DataType; + text: string; + commonParameters?: ParameterName[]; + hasAdvancedParameters?: boolean; + hasMultiFields?: boolean; +} + +export const dataTypesConfig: DataTypeConfig[] = [ + { + value: 'text', + text: 'Text', + commonParameters: ['store', 'index', 'doc_values'], + }, + { + value: 'keyword', + text: 'Keyword', + commonParameters: ['store', 'doc_values'], + }, + { + value: 'object', + text: 'Object', + }, + { + value: 'array', + text: 'Array', + }, +]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts new file mode 100644 index 0000000000000..4d72c0c4c9984 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts @@ -0,0 +1,45 @@ +/* + * 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 { + FormSchema, + FIELD_TYPES, +} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; + +const propertiesArrayToObject = (properties: any[]): any => + properties.reduce((acc, property) => { + const { name, ...rest } = property; + acc[property.name] = rest; + return acc; + }, {}); + +const propertiesObjectToArray = (properties: any): any[] => + Object.entries(properties).map(([name, property]) => ({ name, ...property })); + +export const schema: FormSchema = { + dynamic: { + label: 'Dynamic field', + helpText: 'Allow new fields discovery in document.', + type: FIELD_TYPES.SELECT, + defaultValue: true, + }, + date_detection: { + label: 'Date detection', + helpText: 'Check if the string field is a date.', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + numeric_detection: { + label: 'Numeric field', + helpText: 'Check if the string field is a numeric value.', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + properties: { + inputSerializer: propertiesObjectToArray, + outputSerializer: propertiesArrayToObject, + }, +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts new file mode 100644 index 0000000000000..58cc326b26c00 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts @@ -0,0 +1,12 @@ +/* + * 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 const propertiesArrayToObject = (properties: any[]): any => + properties.reduce((acc, property) => { + const { name, ...rest } = property; + acc[property.name] = rest; + return acc; + }, {}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index cfc3aba4314c1..6d40ad47f3d9a 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,11 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, Fragment } from 'react'; -import { EuiCodeEditor, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import React, { useEffect, Fragment } from 'react'; +import { EuiTitle, EuiSpacer, EuiButton } from '@elastic/eui'; +import { + useForm, + UseField, + UseArray, +} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { + FormRow, + Field, +} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; + +import { schema } from './form.schema'; +import { MappingsProperty } from './components'; +import { propertiesArrayToObject } from './helpers'; interface Props { - setGetDataHandler: (handler: () => { isValid: boolean; data: Mappings }) => void; + setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: Mappings }>) => void; FormattedMessage: typeof ReactIntl.FormattedMessage; defaultValue?: Mappings; areErrorsVisible?: boolean; @@ -18,81 +31,72 @@ export interface Mappings { [key: string]: any; } +const serializeData = (data: Record): Record => ({ + ...data, + properties: propertiesArrayToObject(data.properties as any[]), +}); + export const MappingsEditor = ({ setGetDataHandler, FormattedMessage, areErrorsVisible = true, defaultValue = {}, }: Props) => { - const [mappings, setMappings] = useState(JSON.stringify(defaultValue, null, 2)); - const [error, setError] = useState(null); - - const getFormData = () => { - setError(null); - try { - const parsed: Mappings = JSON.parse(mappings); - return { - data: parsed, - isValid: true, - }; - } catch (e) { - setError(e.message); - return { - isValid: false, - data: {}, - }; - } - }; + const { form } = useForm({ schema }); useEffect(() => { - setGetDataHandler(getFormData); - }, [mappings]); + setGetDataHandler(async () => { + const { data, isValid } = await form.onSubmit(); + return { data: serializeData(data), isValid }; + }); + }, [form]); return ( - - } - onChange={(value: string) => { - setMappings(value); - }} - data-test-subj="mappingsEditor" - /> - {areErrorsVisible && error && ( - - - + + + + + + {/* Mappings properties */} + +

Properties

+
+ + + + {({ rows, addRow, removeRow }) => ( + + {rows.map(({ id, rowPath, isNew }) => ( + removeRow(id)} /> - } - color="danger" - iconType="alert" - > -

{error}

-
-
- )} + ))} + + + Add property + +
+ )} + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts new file mode 100644 index 0000000000000..651fc909d7239 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts @@ -0,0 +1,70 @@ +/* + * 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 { + FIELD_TYPES, + FieldConfig, +} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { + emptyField, + containsCharsField, +} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/field_validators'; + +export type ParameterName = 'name' | 'type' | 'store' | 'index' | 'doc_values'; + +export interface Parameter { + fieldConfig: FieldConfig; + docs?: string; +} + +export const parameters: { [key in ParameterName]: Parameter } = { + name: { + fieldConfig: { + label: 'Name', + validations: [ + { + validator: emptyField, + message: 'Please give a name to the property', + }, + { + validator: containsCharsField(' '), + message: 'Spaces are not allowed in the name.', + }, + ], + }, + }, + type: { + fieldConfig: { + label: 'Type', + defaultValue: 'text', + type: FIELD_TYPES.SELECT, + }, + }, + store: { + fieldConfig: { + label: 'Store', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html', + }, + index: { + fieldConfig: { + label: 'Index', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + // docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html', + }, + doc_values: { + fieldConfig: { + label: 'Doc values', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', + }, +}; From bb98a2e6077f46ace1c1ab20f84367fdafc545de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 11 Jul 2019 11:39:04 +0200 Subject: [PATCH 03/64] Nested properties for "object" data type --- .../components/mappings_property.tsx | 26 ++++++++++++- .../components/property_common_parameters.tsx | 35 ++++++++++------- .../mappings_editor/data_types_config.ts | 14 +++++-- .../ui/components/mappings_editor/helpers.ts | 3 ++ .../mappings_editor/mappings_editor.tsx | 10 +++-- .../components/mappings_editor/parameters.ts | 38 ++++++++++++++++++- 6 files changed, 102 insertions(+), 24 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx index f7e30315c7a82..e359148a3621d 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx @@ -9,11 +9,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiSelect, + EuiButton, EuiButtonIcon, EuiFormRow, } from '@elastic/eui'; import { UseField, + UseArray, Form, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; @@ -35,7 +37,7 @@ export const MappingsProperty = ({ onRemove, fieldPathPrefix = '', form }: Props return ( - + - + + {propertyType.value === 'object' && ( + + {({ rows, addRow, removeRow }) => ( + + {rows.map(({ id, rowPath, isNew }) => ( + removeRow(id)} + /> + ))} + + + Add property + + + )} + + )} ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx index ac8c0476bf5e9..fb7567f101b9e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { @@ -13,7 +13,7 @@ import { } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; import { DataTypeConfig } from '../data_types_config'; -import { parameters } from '../parameters'; +import { parameters, ParameterName } from '../parameters'; interface Props { form: Form; @@ -21,22 +21,31 @@ interface Props { fieldPathPrefix?: string; } +const splitParametersIntoRows = (params: ParameterName[] | ParameterName[][]): ParameterName[][] => + Array.isArray(params[0]) ? (params as ParameterName[][]) : ([params] as ParameterName[][]); + export const PropertyCommonParameters = ({ form, typeConfig, fieldPathPrefix = '' }: Props) => { if (!typeConfig || !typeConfig.commonParameters) { return null; } return ( - - {typeConfig.commonParameters.map(parameter => ( - - - + + {splitParametersIntoRows(typeConfig.commonParameters).map((row, i) => ( +
+ + {row.map(parameter => ( + + + + ))} + +
))} -
+ ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts index efbb0718e3c3c..9e5c985f79a8c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts @@ -6,12 +6,12 @@ import { ParameterName } from './parameters'; -export type DataType = 'text' | 'keyword' | 'object' | 'array' | 'boolean'; +export type DataType = 'text' | 'keyword' | 'numeric' | 'object' | 'array' | 'boolean'; export interface DataTypeConfig { value: DataType; text: string; - commonParameters?: ParameterName[]; + commonParameters?: ParameterName[] | ParameterName[][]; hasAdvancedParameters?: boolean; hasMultiFields?: boolean; } @@ -25,7 +25,15 @@ export const dataTypesConfig: DataTypeConfig[] = [ { value: 'keyword', text: 'Keyword', - commonParameters: ['store', 'doc_values'], + commonParameters: ['store', 'index', 'doc_values'], + }, + { + value: 'numeric', + text: 'Numeric', + commonParameters: [ + ['store', 'index', 'coerce', 'doc_values', 'ignore_malformed'], + ['null_value', 'boost'], + ], }, { value: 'object', diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts index 58cc326b26c00..671699ab4cd86 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts @@ -8,5 +8,8 @@ export const propertiesArrayToObject = (properties: any[]): any => properties.reduce((acc, property) => { const { name, ...rest } = property; acc[property.name] = rest; + if (rest.type === 'object' && rest.properties) { + rest.properties = propertiesArrayToObject(rest.properties); + } return acc; }, {}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 6d40ad47f3d9a..29f7f3e2b11a6 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -31,10 +31,12 @@ export interface Mappings { [key: string]: any; } -const serializeData = (data: Record): Record => ({ - ...data, - properties: propertiesArrayToObject(data.properties as any[]), -}); +const serializeData = (data: Record): Record => { + return { + ...data, + properties: propertiesArrayToObject(data.properties as any[]), + }; +}; export const MappingsEditor = ({ setGetDataHandler, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts index 651fc909d7239..d9c13614a7054 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts @@ -13,7 +13,16 @@ import { containsCharsField, } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/field_validators'; -export type ParameterName = 'name' | 'type' | 'store' | 'index' | 'doc_values'; +export type ParameterName = + | 'name' + | 'type' + | 'store' + | 'index' + | 'doc_values' + | 'coerce' + | 'ignore_malformed' + | 'null_value' + | 'boost'; export interface Parameter { fieldConfig: FieldConfig; @@ -57,7 +66,6 @@ export const parameters: { [key in ParameterName]: Parameter } = { type: FIELD_TYPES.TOGGLE, defaultValue: true, }, - // docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html', }, doc_values: { fieldConfig: { @@ -67,4 +75,30 @@ export const parameters: { [key in ParameterName]: Parameter } = { }, docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', }, + coerce: { + fieldConfig: { + label: 'Coerce', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + }, + ignore_malformed: { + fieldConfig: { + label: 'Ignore malformed', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + }, + null_value: { + fieldConfig: { + label: 'Null value', + type: FIELD_TYPES.TEXT, + }, + }, + boost: { + fieldConfig: { + label: 'Boost', + type: FIELD_TYPES.TEXT, + }, + }, }; From 9c4cee97ef9d86f84b94e20a4b25a5c6a266026e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 12 Jul 2019 13:49:47 +0200 Subject: [PATCH 04/64] Add FormDataProvider to listen to data change --- .../components/mappings_editor/_styles.scss | 49 ++++++++ .../mappings_editor/components/index.ts | 2 +- .../components/mappings_property.tsx | 110 ----------------- .../components/properties_manager.tsx | 47 +++++++ ...ters.tsx => property_basic_parameters.tsx} | 22 ++-- .../components/property_editor.tsx | 116 ++++++++++++++++++ .../config/data_types_definition.ts | 46 +++++++ .../mappings_editor/config/index.ts | 8 ++ .../parameters_definition.ts} | 22 +++- .../mappings_editor/data_types_config.ts | 46 ------- .../ui/components/mappings_editor/helpers.ts | 21 ++-- .../mappings_editor/mappings_editor.tsx | 35 ++---- 12 files changed, 319 insertions(+), 205 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/{property_common_parameters.tsx => property_basic_parameters.tsx} (63%) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/{parameters.ts => config/parameters_definition.ts} (78%) delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss new file mode 100644 index 0000000000000..519683db5ee15 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -0,0 +1,49 @@ +.mappings-editor { + .tree, + .tree ul { + margin:0 0 0 1em; /* indentation */ + padding:0; + list-style:none; + position:relative; + } + + .tree ul {margin-left:.5em} /* (indentation/2) */ + + .tree:before, + .tree ul:before { + content:""; + display:block; + width:0; + position:absolute; + top:0; + bottom:0; + left:0; + border-left:1px solid; + } + + .tree li { + margin:0; + padding-left: 1.5em; /* indentation + .5em */ + line-height:2em; /* default list item's `line-height` */ + position:relative; + } + + .tree li:before { + content:""; + display:block; + width:10px; /* same with indentation */ + height:0; + border-top:1px solid; + margin-top:-1px; /* border top width */ + position:absolute; + top: 64px; + left:0; + } + + .tree li:last-child:before { + background:white; /* same with body background */ + height:auto; + top: 64px; + bottom:0; + } +} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts index 91f38eb171a06..56ee889992009 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './mappings_property'; +export * from './properties_manager'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx deleted file mode 100644 index e359148a3621d..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/mappings_property.tsx +++ /dev/null @@ -1,110 +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 React, { Fragment, useState } from 'react'; -import { - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiSelect, - EuiButton, - EuiButtonIcon, - EuiFormRow, -} from '@elastic/eui'; -import { - UseField, - UseArray, - Form, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; - -import { parameters } from '../parameters'; -import { dataTypesConfig, DataTypeConfig } from '../data_types_config'; -import { PropertyCommonParameters } from './property_common_parameters'; - -interface Props { - form: Form; - onRemove: () => void; - fieldPathPrefix?: string; -} - -export const MappingsProperty = ({ onRemove, fieldPathPrefix = '', form }: Props) => { - const [propertyType, setPropertyType] = useState( - dataTypesConfig.find(config => config.value === 'text')! - ); - - return ( - - - - - - - - {field => ( - - { - setPropertyType(dataTypesConfig.find(config => config.value === value)!); - field.setValue(value); - }} - hasNoInitialSelection={true} - isInvalid={false} - options={dataTypesConfig.map(({ value, text }) => ({ value, text }))} - /> - - )} - - - - - - - - - - {propertyType.value === 'object' && ( - - {({ rows, addRow, removeRow }) => ( - - {rows.map(({ id, rowPath, isNew }) => ( - removeRow(id)} - /> - ))} - - - Add property - - - )} - - )} - - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx new file mode 100644 index 0000000000000..e00cecf179e67 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -0,0 +1,47 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiSpacer, EuiButton } from '@elastic/eui'; + +import { + UseArray, + Form, +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; + +import { PropertyEditor } from './property_editor'; + +interface Props { + form: Form; + path?: string; +} + +export const PropertiesManager = ({ form, path = 'properties' }: Props) => { + return ( + + + {({ rows, addRow, removeRow }) => ( +
    + {rows.map(({ id, rowPath, isNew }) => ( +
  • + removeRow(id)} + /> +
  • + ))} + + + Add property + +
+ )} +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx similarity index 63% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx index fb7567f101b9e..2ab0f89244bbc 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_common_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx @@ -12,33 +12,37 @@ import { Form, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; -import { DataTypeConfig } from '../data_types_config'; -import { parameters, ParameterName } from '../parameters'; +import { parametersDefinition, ParameterName, DataTypeDefinition } from '../config'; interface Props { form: Form; - typeConfig: DataTypeConfig | null; + typeDefinition: DataTypeDefinition | null; fieldPathPrefix?: string; } -const splitParametersIntoRows = (params: ParameterName[] | ParameterName[][]): ParameterName[][] => +const parametersToRows = (params: ParameterName[] | ParameterName[][]): ParameterName[][] => Array.isArray(params[0]) ? (params as ParameterName[][]) : ([params] as ParameterName[][]); -export const PropertyCommonParameters = ({ form, typeConfig, fieldPathPrefix = '' }: Props) => { - if (!typeConfig || !typeConfig.commonParameters) { +export const PropertyBasicParameters = ({ form, typeDefinition, fieldPathPrefix = '' }: Props) => { + if (!typeDefinition || !typeDefinition.basicParameters) { return null; } + + const rows = parametersToRows(typeDefinition.basicParameters); + return ( - {splitParametersIntoRows(typeConfig.commonParameters).map((row, i) => ( + {rows.map((parameters, i) => (
- {row.map(parameter => ( + {parameters.map(parameter => ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx new file mode 100644 index 0000000000000..7480ee13c2648 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -0,0 +1,116 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; +import { + UseField, + Form, + FormDataProvider, +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; + +import { parametersDefinition, dataTypesDefinition, DataType } from '../config'; +import { PropertyBasicParameters } from './property_basic_parameters'; +import { PropertiesManager } from './properties_manager'; + +interface Props { + form: Form; + onRemove: () => void; + fieldPathPrefix?: string; +} + +const hasNestedProperties = (selectedDatatype: string) => + selectedDatatype === 'object' || selectedDatatype === 'nested'; + +export const PropertyEditor = ({ onRemove, fieldPathPrefix = '', form }: Props) => { + return ( + + {formData => { + const selectedDatatype = formData[`${fieldPathPrefix}type`] as DataType; + const typeDefinition = dataTypesDefinition[selectedDatatype]; + + return ( + + + {/* Field name */} + + + + + {/* Field type */} + + ({ + value, + text: label, + })), + }, + }} + /> + + + {/* Field configuration (if any) */} + {typeDefinition.configuration && + typeDefinition.configuration.map((parameter, i) => ( + + + + ))} + + + {/* Delete field button */} + + + + + + + + {/* Basic parameters for the selected type */} + + + {hasNestedProperties(selectedDatatype) && ( + + + + + )} + + + + ); + }} + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts new file mode 100644 index 0000000000000..5626998cfc57d --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts @@ -0,0 +1,46 @@ +/* + * 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 { ParameterName } from './parameters_definition'; + +export type DataType = 'text' | 'keyword' | 'numeric' | 'object' | 'nested' | 'array'; + +export interface DataTypeDefinition { + label: string; + configuration?: ParameterName[]; + basicParameters?: ParameterName[] | ParameterName[][]; + hasAdvancedParameters?: boolean; + hasMultiFields?: boolean; +} + +export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { + text: { + label: 'Text', + basicParameters: ['store', 'index', 'doc_values'], + }, + keyword: { + label: 'Keyword', + basicParameters: ['store', 'index', 'doc_values'], + }, + numeric: { + label: 'Numeric', + basicParameters: [ + ['store', 'index', 'coerce', 'doc_values', 'ignore_malformed'], + ['null_value', 'boost'], + ], + }, + object: { + label: 'Object', + configuration: ['dynamic', 'enabled'], + }, + nested: { + label: 'Nested', + configuration: ['dynamic'], + }, + array: { + label: 'Array', + }, +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts new file mode 100644 index 0000000000000..35dab683cff24 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './data_types_definition'; +export * from './parameters_definition'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts similarity index 78% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index d9c13614a7054..296958ea3be32 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/parameters.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -7,11 +7,11 @@ import { FIELD_TYPES, FieldConfig, -} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { emptyField, containsCharsField, -} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/field_validators'; +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/field_validators'; export type ParameterName = | 'name' @@ -22,6 +22,8 @@ export type ParameterName = | 'coerce' | 'ignore_malformed' | 'null_value' + | 'dynamic' + | 'enabled' | 'boost'; export interface Parameter { @@ -29,7 +31,7 @@ export interface Parameter { docs?: string; } -export const parameters: { [key in ParameterName]: Parameter } = { +export const parametersDefinition: { [key in ParameterName]: Parameter } = { name: { fieldConfig: { label: 'Name', @@ -101,4 +103,18 @@ export const parameters: { [key in ParameterName]: Parameter } = { type: FIELD_TYPES.TEXT, }, }, + dynamic: { + fieldConfig: { + label: 'Dynamic', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + }, + enabled: { + fieldConfig: { + label: 'Enabled', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + }, }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts deleted file mode 100644 index 9e5c985f79a8c..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/data_types_config.ts +++ /dev/null @@ -1,46 +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 { ParameterName } from './parameters'; - -export type DataType = 'text' | 'keyword' | 'numeric' | 'object' | 'array' | 'boolean'; - -export interface DataTypeConfig { - value: DataType; - text: string; - commonParameters?: ParameterName[] | ParameterName[][]; - hasAdvancedParameters?: boolean; - hasMultiFields?: boolean; -} - -export const dataTypesConfig: DataTypeConfig[] = [ - { - value: 'text', - text: 'Text', - commonParameters: ['store', 'index', 'doc_values'], - }, - { - value: 'keyword', - text: 'Keyword', - commonParameters: ['store', 'index', 'doc_values'], - }, - { - value: 'numeric', - text: 'Numeric', - commonParameters: [ - ['store', 'index', 'coerce', 'doc_values', 'ignore_malformed'], - ['null_value', 'boost'], - ], - }, - { - value: 'object', - text: 'Object', - }, - { - value: 'array', - text: 'Array', - }, -]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts index 671699ab4cd86..fb2ad66b517b3 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export const propertiesArrayToObject = (properties: any[]): any => - properties.reduce((acc, property) => { - const { name, ...rest } = property; - acc[property.name] = rest; - if (rest.type === 'object' && rest.properties) { - rest.properties = propertiesArrayToObject(rest.properties); - } - return acc; - }, {}); +export const propertiesArrayToObject = (properties?: any[]): any => + properties + ? properties.reduce((acc, property) => { + const { name, ...rest } = property; + acc[property.name] = rest; + if (rest.properties) { + // Recursively convert Array to Object + rest.properties = propertiesArrayToObject(rest.properties); + } + return acc; + }, {}) + : properties; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 29f7f3e2b11a6..0dcc8f9d09f74 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, Fragment } from 'react'; -import { EuiTitle, EuiSpacer, EuiButton } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { useForm, UseField, - UseArray, } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { FormRow, @@ -17,7 +16,7 @@ import { } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; import { schema } from './form.schema'; -import { MappingsProperty } from './components'; +import { PropertiesManager } from './components'; import { propertiesArrayToObject } from './helpers'; interface Props { @@ -54,7 +53,7 @@ export const MappingsEditor = ({ }, [form]); return ( - +
{/* Global Mappings configuration */} - {/* Mappings properties */} + {/* Document fields */} -

Properties

+

Document fields

- - - {({ rows, addRow, removeRow }) => ( - - {rows.map(({ id, rowPath, isNew }) => ( - removeRow(id)} - /> - ))} - - - Add property - - - )} - - + +
); }; From 857663280799efe9e8578364b7cc1f7e0632659d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 12 Jul 2019 17:10:50 +0200 Subject: [PATCH 05/64] Add support for "array" data type --- .../index_management/public/index.scss | 3 +- .../index_management/static/ui/_styles.scss | 1 + .../components/mappings_editor/_styles.scss | 4 +- .../components/properties_manager.tsx | 1 - .../components/property_editor.tsx | 102 ++++++++++++------ .../mappings_editor/mappings_editor.tsx | 2 +- 6 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/_styles.scss diff --git a/x-pack/legacy/plugins/index_management/public/index.scss b/x-pack/legacy/plugins/index_management/public/index.scss index 939c9fd840421..1589f121c6965 100644 --- a/x-pack/legacy/plugins/index_management/public/index.scss +++ b/x-pack/legacy/plugins/index_management/public/index.scss @@ -10,4 +10,5 @@ // indChart__legend--small // indChart__legend-isLoading -@import 'index_management'; \ No newline at end of file +@import 'index_management'; +@import '../static/ui/styles' diff --git a/x-pack/legacy/plugins/index_management/static/ui/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/_styles.scss new file mode 100644 index 0000000000000..70d8c2e1977d2 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/_styles.scss @@ -0,0 +1 @@ +@import "./components/mappings_editor/styles" diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index 519683db5ee15..d9614d70e8893 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -18,7 +18,7 @@ top:0; bottom:0; left:0; - border-left:1px solid; + border-left:1px dotted; } .tree li { @@ -33,7 +33,7 @@ display:block; width:10px; /* same with indentation */ height:0; - border-top:1px solid; + border-top:1px dotted; margin-top:-1px; /* border top width */ position:absolute; top: 64px; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index e00cecf179e67..03d7709059243 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -34,7 +34,6 @@ export const PropertiesManager = ({ form, path = 'properties' }: Props) => { /> ))} - Add property diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 7480ee13c2648..5aea3c3081b24 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -18,14 +18,46 @@ import { PropertiesManager } from './properties_manager'; interface Props { form: Form; - onRemove: () => void; + onRemove?: () => void; fieldPathPrefix?: string; + isDeletable?: boolean; + isAnonymous?: boolean; } -const hasNestedProperties = (selectedDatatype: string) => +const hasNestedProperties = (selectedDatatype: DataType) => selectedDatatype === 'object' || selectedDatatype === 'nested'; -export const PropertyEditor = ({ onRemove, fieldPathPrefix = '', form }: Props) => { +export const PropertyEditor = ({ + form, + fieldPathPrefix = '', + onRemove = () => undefined, + isAnonymous = false, + isDeletable = true, +}: Props) => { + const renderNestedProperties = (selectedType: DataType) => { + if (selectedType === 'array') { + return ( +
    +
  • + +
  • +
+ ); + } + + return hasNestedProperties(selectedType) ? ( + + + + + ) : null; + }; + return ( {formData => { @@ -34,16 +66,18 @@ export const PropertyEditor = ({ onRemove, fieldPathPrefix = '', form }: Props) return ( - + {/* Field name */} - - - + {isAnonymous !== true && ( + + + + )} {/* Field type */} @@ -64,7 +98,8 @@ export const PropertyEditor = ({ onRemove, fieldPathPrefix = '', form }: Props) {/* Field configuration (if any) */} - {typeDefinition.configuration && + {typeDefinition && + typeDefinition.configuration && typeDefinition.configuration.map((parameter, i) => ( {/* Delete field button */} - - - + {isDeletable && ( + + + + )} - - - {/* Basic parameters for the selected type */} - - - {hasNestedProperties(selectedDatatype) && ( + {typeDefinition && typeDefinition.basicParameters && ( - - + + + {/* Basic parameters for the selected type */} + )} + {renderNestedProperties(selectedDatatype)} + ); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 0dcc8f9d09f74..b0f118ae32dec 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -78,7 +78,7 @@ export const MappingsEditor = ({

Document fields

- +
); From f220709a8fbdcee674549b497de372dfb2563885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 17 Jul 2019 12:16:16 +0200 Subject: [PATCH 06/64] Add serializer & deSerializer to form --- .../components/property_editor.tsx | 4 +-- .../components/mappings_editor/form.schema.ts | 14 ---------- .../ui/components/mappings_editor/helpers.ts | 23 +++++++++++++++- .../mappings_editor/mappings_editor.tsx | 26 +++++++++---------- 4 files changed, 36 insertions(+), 31 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 5aea3c3081b24..727c10fcc4c63 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -13,6 +13,7 @@ import { import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; import { parametersDefinition, dataTypesDefinition, DataType } from '../config'; +import { hasNestedProperties } from '../helpers'; import { PropertyBasicParameters } from './property_basic_parameters'; import { PropertiesManager } from './properties_manager'; @@ -24,9 +25,6 @@ interface Props { isAnonymous?: boolean; } -const hasNestedProperties = (selectedDatatype: DataType) => - selectedDatatype === 'object' || selectedDatatype === 'nested'; - export const PropertyEditor = ({ form, fieldPathPrefix = '', diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts index 4d72c0c4c9984..7f5f866ace43b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts @@ -9,16 +9,6 @@ import { FIELD_TYPES, } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -const propertiesArrayToObject = (properties: any[]): any => - properties.reduce((acc, property) => { - const { name, ...rest } = property; - acc[property.name] = rest; - return acc; - }, {}); - -const propertiesObjectToArray = (properties: any): any[] => - Object.entries(properties).map(([name, property]) => ({ name, ...property })); - export const schema: FormSchema = { dynamic: { label: 'Dynamic field', @@ -38,8 +28,4 @@ export const schema: FormSchema = { type: FIELD_TYPES.TOGGLE, defaultValue: true, }, - properties: { - inputSerializer: propertiesObjectToArray, - outputSerializer: propertiesArrayToObject, - }, }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts index fb2ad66b517b3..9949913d511f4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts @@ -3,16 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { DataType } from './config'; + +export const hasNestedProperties = (selectedDatatype: DataType) => + selectedDatatype === 'object' || selectedDatatype === 'nested'; export const propertiesArrayToObject = (properties?: any[]): any => properties ? properties.reduce((acc, property) => { const { name, ...rest } = property; acc[property.name] = rest; - if (rest.properties) { + if (hasNestedProperties(rest.type) && rest.properties) { // Recursively convert Array to Object rest.properties = propertiesArrayToObject(rest.properties); } return acc; }, {}) : properties; + +export const propertiesObjectToArray = ( + properties: { + [key: string]: Record; + } = {} +): any[] => + Object.entries(properties).map(([name, property]) => { + if (hasNestedProperties(property.type) && property.properties) { + // Recursively convert Object to Array + return { + name, + ...property, + properties: propertiesObjectToArray(property.properties), + }; + } + return { name, ...property }; + }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index b0f118ae32dec..fdf5bd185cfd2 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -17,7 +17,7 @@ import { import { schema } from './form.schema'; import { PropertiesManager } from './components'; -import { propertiesArrayToObject } from './helpers'; +import { propertiesArrayToObject, propertiesObjectToArray } from './helpers'; interface Props { setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: Mappings }>) => void; @@ -30,26 +30,26 @@ export interface Mappings { [key: string]: any; } -const serializeData = (data: Record): Record => { - return { - ...data, - properties: propertiesArrayToObject(data.properties as any[]), - }; -}; +const serializer = (data: Record): Record => ({ + ...data, + properties: propertiesArrayToObject(data.properties as any[]), +}); + +const deSerializer = (data: Record): Record => ({ + ...data, + properties: propertiesObjectToArray(data.properties as { [key: string]: any }), +}); export const MappingsEditor = ({ setGetDataHandler, FormattedMessage, areErrorsVisible = true, - defaultValue = {}, + defaultValue, }: Props) => { - const { form } = useForm({ schema }); + const { form } = useForm({ schema, serializer, deSerializer, defaultValue }); useEffect(() => { - setGetDataHandler(async () => { - const { data, isValid } = await form.onSubmit(); - return { data: serializeData(data), isValid }; - }); + setGetDataHandler(form.onSubmit); }, [form]); return ( From 27ff0562d1ab73913bff8060726e02beaefe45d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 25 Jul 2019 08:38:30 +0200 Subject: [PATCH 07/64] Use checkbox instead of toggle --- .../components/property_editor.tsx | 2 +- .../config/parameters_definition.ts | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 727c10fcc4c63..ed194f9e6b3a7 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -64,7 +64,7 @@ export const PropertyEditor = ({ return ( - + {/* Field name */} {isAnonymous !== true && ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 296958ea3be32..578599075f024 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -5,13 +5,14 @@ */ import { - FIELD_TYPES, FieldConfig, + FIELD_TYPES, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; + import { emptyField, containsCharsField, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/field_validators'; +} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; export type ParameterName = | 'name' @@ -37,12 +38,13 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { label: 'Name', validations: [ { - validator: emptyField, - message: 'Please give a name to the property', + validator: emptyField('Please give a name to the property'), }, { - validator: containsCharsField(' '), - message: 'Spaces are not allowed in the name.', + validator: containsCharsField({ + chars: ' ', + message: 'Spaces are not allowed in the name.', + }), }, ], }, @@ -57,7 +59,7 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { store: { fieldConfig: { label: 'Store', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html', @@ -65,14 +67,14 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { index: { fieldConfig: { label: 'Index', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, }, doc_values: { fieldConfig: { label: 'Doc values', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', @@ -80,14 +82,14 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { coerce: { fieldConfig: { label: 'Coerce', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, }, ignore_malformed: { fieldConfig: { label: 'Ignore malformed', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, }, @@ -106,14 +108,14 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { dynamic: { fieldConfig: { label: 'Dynamic', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, }, enabled: { fieldConfig: { label: 'Enabled', - type: FIELD_TYPES.TOGGLE, + type: FIELD_TYPES.CHECKBOX, defaultValue: true, }, }, From e453deaf76ab7db9ae40aceaaeb304f7da4aa12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 25 Jul 2019 08:45:35 +0200 Subject: [PATCH 08/64] Add dynamic dates format --- .../static/ui/components/mappings_editor/form.schema.ts | 6 ++++++ .../ui/components/mappings_editor/mappings_editor.tsx | 1 + 2 files changed, 7 insertions(+) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts index 7f5f866ace43b..7cbbe13fe0904 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts @@ -28,4 +28,10 @@ export const schema: FormSchema = { type: FIELD_TYPES.TOGGLE, defaultValue: true, }, + dynamic_date_formats: { + label: 'Dynamic dates format', + helpText: 'The dynamic_date_formats can be customised to support your own date formats.', + type: FIELD_TYPES.COMBO_BOX, + defaultValue: [], + }, }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index fdf5bd185cfd2..d82344c745f53 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -72,6 +72,7 @@ export const MappingsEditor = ({ /> + {/* Document fields */} From eefe40bcf20477a0da3519c4e74c980de870dc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 25 Jul 2019 08:52:20 +0200 Subject: [PATCH 09/64] Remove margin left on first level of nested properties --- .../mappings_editor/components/properties_manager.tsx | 5 +++-- .../static/ui/components/mappings_editor/mappings_editor.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index 03d7709059243..5c070feb3ee2b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -16,15 +16,16 @@ import { PropertyEditor } from './property_editor'; interface Props { form: Form; + depthLevel?: string; path?: string; } -export const PropertiesManager = ({ form, path = 'properties' }: Props) => { +export const PropertiesManager = ({ form, depthLevel, path = 'properties' }: Props) => { return ( {({ rows, addRow, removeRow }) => ( -
    +
      {rows.map(({ id, rowPath, isNew }) => (
    • Document fields - + ); }; From a9d5a3e63f04c05e62ffc18b6ae5cff32f913675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 25 Jul 2019 11:12:37 +0200 Subject: [PATCH 10/64] Allow subType to be declared for a type --- .../components/property_editor.tsx | 23 ++++++++++++ .../config/data_types_definition.ts | 34 +++++++++++++++++- .../config/parameters_definition.ts | 9 +++++ .../mappings_editor/mappings_editor.tsx | 35 +++++++++++++++++-- 4 files changed, 98 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index ed194f9e6b3a7..a9a74c02e6663 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -95,6 +95,29 @@ export const PropertyEditor = ({ /> + {/* Field sub type (if any) */} + {typeDefinition && typeDefinition.subTypes && ( + + ({ + value: type, + text: type, + })), + }, + }} + /> + + )} + {/* Field configuration (if any) */} {typeDefinition && typeDefinition.configuration && diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts index 5626998cfc57d..4457389cd8333 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts @@ -8,8 +8,21 @@ import { ParameterName } from './parameters_definition'; export type DataType = 'text' | 'keyword' | 'numeric' | 'object' | 'nested' | 'array'; +export type SubType = NumericType; + +export type NumericType = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'double' + | 'float' + | 'half_float' + | 'scaled_float'; + export interface DataTypeDefinition { label: string; + subTypes?: { label: string; types: SubType[] }; configuration?: ParameterName[]; basicParameters?: ParameterName[] | ParameterName[][]; hasAdvancedParameters?: boolean; @@ -19,7 +32,7 @@ export interface DataTypeDefinition { export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { text: { label: 'Text', - basicParameters: ['store', 'index', 'doc_values'], + basicParameters: ['store', 'index', 'fielddata'], }, keyword: { label: 'Keyword', @@ -27,6 +40,10 @@ export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { }, numeric: { label: 'Numeric', + subTypes: { + label: 'Numeric type', + types: ['long', 'integer', 'short', 'byte', 'double', 'float', 'half_float', 'scaled_float'], + }, basicParameters: [ ['store', 'index', 'coerce', 'doc_values', 'ignore_malformed'], ['null_value', 'boost'], @@ -44,3 +61,18 @@ export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { label: 'Array', }, }; + +const subTypesMapToType = Object.entries(dataTypesDefinition).reduce( + (acc, [type, definition]) => { + if ({}.hasOwnProperty.call(definition, 'subTypes')) { + definition.subTypes!.types.forEach(subType => { + acc[subType] = type; + }); + } + return acc; + }, + {} as Record +); + +export const getTypeFromSubType = (subType: SubType): DataType => + subTypesMapToType[subType] as DataType; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 578599075f024..5bd80c44ee715 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -19,6 +19,7 @@ export type ParameterName = | 'type' | 'store' | 'index' + | 'fielddata' | 'doc_values' | 'coerce' | 'ignore_malformed' @@ -79,6 +80,14 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { }, docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', }, + fielddata: { + fieldConfig: { + label: 'Fielddata', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', + }, coerce: { fieldConfig: { label: 'Coerce', diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 0494f3e363591..dd94edfc76d1e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -18,6 +18,7 @@ import { import { schema } from './form.schema'; import { PropertiesManager } from './components'; import { propertiesArrayToObject, propertiesObjectToArray } from './helpers'; +import { dataTypesDefinition, getTypeFromSubType } from './config'; interface Props { setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: Mappings }>) => void; @@ -30,14 +31,44 @@ export interface Mappings { [key: string]: any; } +const serializeProperties = (properties: any[]) => + properties.map(prop => { + // If a subType is present, use it as type for ES + if ({}.hasOwnProperty.call(prop, 'subType')) { + prop.type = prop.subType; + delete prop.subType; + } + return prop; + }); + +const deSerializeProperties = (properties: { [key: string]: any }) => { + Object.entries(properties).forEach(([name, prop]: [string, any]) => { + // Check if the type provided is a subType (e.g: "float" is a subType of the "numeric" type in the UI) + if (!(dataTypesDefinition as any)[prop.type]) { + const type = getTypeFromSubType(prop.type); + if (!type) { + throw new Error( + `Property type "${prop.type}" not recognized and no subType was found for it.` + ); + } + prop.subType = prop.type; + prop.type = type; + } + }); + + return properties; +}; + const serializer = (data: Record): Record => ({ ...data, - properties: propertiesArrayToObject(data.properties as any[]), + properties: propertiesArrayToObject(serializeProperties(data.properties as any[])), }); const deSerializer = (data: Record): Record => ({ ...data, - properties: propertiesObjectToArray(data.properties as { [key: string]: any }), + properties: propertiesObjectToArray( + deSerializeProperties(data.properties as { [key: string]: any }) + ), }); export const MappingsEditor = ({ From b063a8d3c07c11741e24dde443278641404867c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 25 Jul 2019 12:32:15 +0200 Subject: [PATCH 11/64] Add date, range, boolean, binary types --- .../components/property_basic_parameters.tsx | 11 +++- .../config/data_types_definition.ts | 54 +++++++++++++++++-- .../config/parameters_definition.ts | 23 +++++++- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx index 2ab0f89244bbc..fd3ee792ece8b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx @@ -30,13 +30,20 @@ export const PropertyBasicParameters = ({ form, typeDefinition, fieldPathPrefix const rows = parametersToRows(typeDefinition.basicParameters); + const getMaxWidth = (rowIndex: number, totalItems: number) => { + if (rowIndex === 0 || totalItems >= 3) { + return 'initial'; + } + return totalItems <= 1 ? '300px' : '600px'; + }; + return ( {rows.map((parameters, i) => (
      - + {parameters.map(parameter => ( - + + options.length ? options.join('||') : undefined, + deSerializer: (formats?: string | any[]): any[] => + Array.isArray(formats) ? formats : (formats as string).split('||'), + }, + }, }; From 4066ae796730a0f352e2954debb137276955c5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 26 Jul 2019 11:39:31 +0200 Subject: [PATCH 12/64] Add nested properties inside accordion --- .../components/properties_manager.tsx | 62 +++++++++++++------ .../components/property_editor.tsx | 43 ++++++------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index 5c070feb3ee2b..d83a4657a8eca 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -5,7 +5,8 @@ */ import React, { Fragment } from 'react'; -import { EuiSpacer, EuiButton } from '@elastic/eui'; +import { EuiSpacer, EuiButton, EuiAccordion } from '@elastic/eui'; +import uuid from 'uuid'; import { UseArray, @@ -18,28 +19,53 @@ interface Props { form: Form; depthLevel?: string; path?: string; + fieldName?: string; } -export const PropertiesManager = ({ form, depthLevel, path = 'properties' }: Props) => { +export const PropertiesManager = ({ + form, + depthLevel, + path = 'properties', + fieldName = '', +}: Props) => { + const renderPropertiesTree = ({ rows, addRow, removeRow }) => ( +
        + {rows.map(({ id, rowPath, isNew }) => { + return ( +
      • + removeRow(id)} + isEditMode={!isNew} + /> +
      • + ); + })} + + Add property + +
      + ); + return ( - {({ rows, addRow, removeRow }) => ( -
        - {rows.map(({ id, rowPath, isNew }) => ( -
      • - removeRow(id)} - /> -
      • - ))} - - Add property - -
      - )} + {({ rows, addRow, removeRow }) => { + if (depthLevel === '0') { + return renderPropertiesTree({ rows, addRow, removeRow }); + } + return ( + + {renderPropertiesTree({ rows, addRow, removeRow })} + + ); + }}
      diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index a9a74c02e6663..788364794ab9c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -23,6 +23,7 @@ interface Props { fieldPathPrefix?: string; isDeletable?: boolean; isAnonymous?: boolean; + isEditMode?: boolean; } export const PropertyEditor = ({ @@ -31,8 +32,9 @@ export const PropertyEditor = ({ onRemove = () => undefined, isAnonymous = false, isDeletable = true, + isEditMode = false, }: Props) => { - const renderNestedProperties = (selectedType: DataType) => { + const renderNestedProperties = (selectedType: DataType, fieldName: string) => { if (selectedType === 'array') { return (
        @@ -51,14 +53,22 @@ export const PropertyEditor = ({ return hasNestedProperties(selectedType) ? ( - + ) : null; }; return ( - + {formData => { + const fieldName = formData[`${fieldPathPrefix}name`] as string; const selectedDatatype = formData[`${fieldPathPrefix}type`] as DataType; const typeDefinition = dataTypesDefinition[selectedDatatype]; @@ -71,18 +81,19 @@ export const PropertyEditor = ({ )} - {/* Field type */} - {/* Field sub type (if any) */} {typeDefinition && typeDefinition.subTypes && ( )} - - {/* Field configuration (if any) */} - {typeDefinition && - typeDefinition.configuration && - typeDefinition.configuration.map((parameter, i) => ( - - - - ))} + {/* Empty flex item to fill the space in between */} {/* Delete field button */} @@ -158,11 +154,12 @@ export const PropertyEditor = ({ form={form} typeDefinition={typeDefinition} fieldPathPrefix={fieldPathPrefix} + isEditMode={isEditMode} /> )} - {renderNestedProperties(selectedDatatype)} + {renderNestedProperties(selectedDatatype, fieldName)} From 7a36eb4af21747b736accdd06d79ecd5cf621227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 26 Jul 2019 11:40:09 +0200 Subject: [PATCH 13/64] Fix defaultValue for parameters --- .../components/property_basic_parameters.tsx | 18 +++++++++++++++++- .../config/parameters_definition.ts | 4 ++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx index fd3ee792ece8b..10bf5ed288f60 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx @@ -17,19 +17,27 @@ import { parametersDefinition, ParameterName, DataTypeDefinition } from '../conf interface Props { form: Form; typeDefinition: DataTypeDefinition | null; + isEditMode?: boolean; fieldPathPrefix?: string; } const parametersToRows = (params: ParameterName[] | ParameterName[][]): ParameterName[][] => Array.isArray(params[0]) ? (params as ParameterName[][]) : ([params] as ParameterName[][]); -export const PropertyBasicParameters = ({ form, typeDefinition, fieldPathPrefix = '' }: Props) => { +export const PropertyBasicParameters = ({ + form, + typeDefinition, + isEditMode = false, + fieldPathPrefix = '', +}: Props) => { if (!typeDefinition || !typeDefinition.basicParameters) { return null; } const rows = parametersToRows(typeDefinition.basicParameters); + // If we have 2 or less items to display, we limit the width + // of the container to limit the size of the . const getMaxWidth = (rowIndex: number, totalItems: number) => { if (rowIndex === 0 || totalItems >= 3) { return 'initial'; @@ -37,6 +45,13 @@ export const PropertyBasicParameters = ({ form, typeDefinition, fieldPathPrefix return totalItems <= 1 ? '300px' : '600px'; }; + const getDefaultValue = (parameter: ParameterName): unknown | undefined => + isEditMode + ? undefined + : parametersDefinition[parameter] && + parametersDefinition[parameter].fieldConfig && + parametersDefinition[parameter].fieldConfig!.defaultValue; + return ( {rows.map((parameters, i) => ( @@ -47,6 +62,7 @@ export const PropertyBasicParameters = ({ form, typeDefinition, fieldPathPrefix Date: Fri, 26 Jul 2019 14:19:15 +0200 Subject: [PATCH 14/64] Rename UseArray child props --- .../components/mappings_editor/_styles.scss | 4 +++ .../components/properties_manager.tsx | 25 +++++++++++------ .../components/property_basic_parameters.tsx | 28 ++++++++++--------- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index d9614d70e8893..7518a1e7192f0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -1,4 +1,8 @@ .mappings-editor { + .euiAccordion__childWrapper { + transition: none; + } + .tree, .tree ul { margin:0 0 0 1em; /* indentation */ diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index d83a4657a8eca..e4a50d8dacd37 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -11,6 +11,7 @@ import uuid from 'uuid'; import { UseArray, Form, + ArrayItem, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { PropertyEditor } from './property_editor'; @@ -28,21 +29,29 @@ export const PropertiesManager = ({ path = 'properties', fieldName = '', }: Props) => { - const renderPropertiesTree = ({ rows, addRow, removeRow }) => ( + const renderPropertiesTree = ({ + items, + addItem, + removeItem, + }: { + items: ArrayItem[]; + addItem: () => void; + removeItem: (id: number) => void; + }) => (
          - {rows.map(({ id, rowPath, isNew }) => { + {items.map(({ id, path: itemPath, isNew }) => { return (
        • removeRow(id)} + onRemove={() => removeItem(id)} isEditMode={!isNew} />
        • ); })} - + Add property
        @@ -51,9 +60,9 @@ export const PropertiesManager = ({ return ( - {({ rows, addRow, removeRow }) => { + {({ items, addItem, removeItem }) => { if (depthLevel === '0') { - return renderPropertiesTree({ rows, addRow, removeRow }); + return renderPropertiesTree({ items, addItem, removeItem }); } return ( - {renderPropertiesTree({ rows, addRow, removeRow })} + {renderPropertiesTree({ items, addItem, removeItem })} ); }} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx index 10bf5ed288f60..539ed26c36f4c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx @@ -57,19 +57,21 @@ export const PropertyBasicParameters = ({ {rows.map((parameters, i) => (
        - {parameters.map(parameter => ( - - - - ))} + {parameters.map(parameter => { + return ( + + + + ); + })}
        ))} From d4caf4e6a8a858388469875d2be123cfb52329ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 26 Jul 2019 15:42:47 +0200 Subject: [PATCH 15/64] Add styles for nested properties --- .../components/mappings_editor/_styles.scss | 16 ++++++++++ .../components/properties_manager.tsx | 32 ++++++++++--------- .../components/property_editor.tsx | 4 +-- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index 7518a1e7192f0..6084a127caba3 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -3,6 +3,15 @@ transition: none; } + .euiAccordion__padding--l { + padding-right: 0; + } + + .property-editor { + border-bottom: 1px solid #eee; + padding-top: 24px; + } + .tree, .tree ul { margin:0 0 0 1em; /* indentation */ @@ -30,6 +39,13 @@ padding-left: 1.5em; /* indentation + .5em */ line-height:2em; /* default list item's `line-height` */ position:relative; + + + &:last-child { + .property-editor { + border-bottom: none; + } + } } .tree li:before { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index e4a50d8dacd37..25a9f2d1eabab 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -38,23 +38,25 @@ export const PropertiesManager = ({ addItem: () => void; removeItem: (id: number) => void; }) => ( -
          - {items.map(({ id, path: itemPath, isNew }) => { - return ( -
        • - removeItem(id)} - isEditMode={!isNew} - /> -
        • - ); - })} + +
            + {items.map(({ id, path: itemPath, isNew }) => { + return ( +
          • + removeItem(id)} + isEditMode={!isNew} + /> +
          • + ); + })} +
          Add property -
        +
        ); return ( @@ -68,7 +70,7 @@ export const PropertiesManager = ({ {renderPropertiesTree({ items, addItem, removeItem })} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 788364794ab9c..df7a33e446525 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -73,7 +73,7 @@ export const PropertyEditor = ({ const typeDefinition = dataTypesDefinition[selectedDatatype]; return ( - +
        {/* Field name */} {isAnonymous !== true && ( @@ -162,7 +162,7 @@ export const PropertyEditor = ({ {renderNestedProperties(selectedDatatype, fieldName)} - +
        ); }}
        From 8c51b765f061aaac797635bbb1392932de23de3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 30 Jul 2019 12:57:15 +0200 Subject: [PATCH 16/64] Style PropertyEditor with hover --- .../components/mappings_editor/_styles.scss | 24 +++++++++++-------- .../components/properties_manager.tsx | 8 +++---- .../components/property_editor.tsx | 22 ++--------------- .../config/data_types_definition.ts | 6 +---- .../config/parameters_definition.ts | 4 ++-- .../components/mappings_editor/constants.ts | 11 +++++++++ .../mappings_editor/mappings_editor.tsx | 19 +++++---------- 7 files changed, 40 insertions(+), 54 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index 6084a127caba3..20453f00619cf 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -9,7 +9,12 @@ .property-editor { border-bottom: 1px solid #eee; - padding-top: 24px; + margin: -12px; + padding: 12px; + + &:hover { + background-color: #fafbfd; + } } .tree, @@ -20,7 +25,7 @@ position:relative; } - .tree ul {margin-left:.5em} /* (indentation/2) */ + .tree ul {margin-left:.5em} .tree:before, .tree ul:before { @@ -31,15 +36,15 @@ top:0; bottom:0; left:0; - border-left:1px dotted; + border-left:1px dotted #aaa; } .tree li { margin:0; - padding-left: 1.5em; /* indentation + .5em */ - line-height:2em; /* default list item's `line-height` */ + padding-left: 1.5em; + line-height:2em; position:relative; - + margin-bottom: 12px; &:last-child { .property-editor { @@ -51,17 +56,16 @@ .tree li:before { content:""; display:block; - width:10px; /* same with indentation */ height:0; - border-top:1px dotted; - margin-top:-1px; /* border top width */ + border-top:1px dotted #aaa; + margin-top:-1px; position:absolute; top: 64px; left:0; } .tree li:last-child:before { - background:white; /* same with body background */ + background:white; height:auto; top: 64px; bottom:0; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index 25a9f2d1eabab..bbdedcf6b62d4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -18,7 +18,7 @@ import { PropertyEditor } from './property_editor'; interface Props { form: Form; - depthLevel?: string; + depthLevel?: number; path?: string; fieldName?: string; } @@ -39,7 +39,7 @@ export const PropertiesManager = ({ removeItem: (id: number) => void; }) => ( -
          +
            {items.map(({ id, path: itemPath, isNew }) => { return (
          • @@ -63,7 +63,8 @@ export const PropertiesManager = ({ {({ items, addItem, removeItem }) => { - if (depthLevel === '0') { + if (depthLevel === 0) { + // At the root level we don't add the accordion return renderPropertiesTree({ items, addItem, removeItem }); } return ( @@ -78,7 +79,6 @@ export const PropertiesManager = ({ ); }} - ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index df7a33e446525..24fb010a94a82 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -34,23 +34,8 @@ export const PropertyEditor = ({ isDeletable = true, isEditMode = false, }: Props) => { - const renderNestedProperties = (selectedType: DataType, fieldName: string) => { - if (selectedType === 'array') { - return ( -
              -
            • - -
            • -
            - ); - } - - return hasNestedProperties(selectedType) ? ( + const renderNestedProperties = (selectedType: DataType, fieldName: string) => + hasNestedProperties(selectedType) ? ( ) : null; - }; return (
      ); }} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts index 517e41123dc66..41ba2c89a3cec 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts @@ -15,8 +15,7 @@ export type DataType = | 'boolean' | 'range' | 'object' - | 'nested' - | 'array'; + | 'nested'; export type SubType = NumericType | DateType | RangeType; @@ -103,9 +102,6 @@ export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { label: 'Nested', basicParameters: ['dynamic'], }, - array: { - label: 'Array', - }, }; const subTypesMapToType = Object.entries(dataTypesDefinition).reduce( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index ed68c5600b845..49714c5d5dea5 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -39,7 +39,7 @@ export interface Parameter { export const parametersDefinition: { [key in ParameterName]: Parameter } = { name: { fieldConfig: { - label: 'Name', + label: 'Field name', defaultValue: '', validations: [ { @@ -56,7 +56,7 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { }, type: { fieldConfig: { - label: 'Type', + label: 'Field type', defaultValue: 'text', type: FIELD_TYPES.SELECT, }, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts new file mode 100644 index 0000000000000..2343b82d6712a --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts @@ -0,0 +1,11 @@ +/* + * 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 const DYNAMIC_SETTING_OPTIONS = [ + { value: true, text: 'true' }, + { value: false, text: 'false' }, + { value: 'strict', text: 'strict' }, +]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index dd94edfc76d1e..04305e7365a01 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiSpacer, EuiForm } from '@elastic/eui'; import { useForm, UseField, @@ -19,10 +19,10 @@ import { schema } from './form.schema'; import { PropertiesManager } from './components'; import { propertiesArrayToObject, propertiesObjectToArray } from './helpers'; import { dataTypesDefinition, getTypeFromSubType } from './config'; +import { DYNAMIC_SETTING_OPTIONS } from './constants'; interface Props { setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: Mappings }>) => void; - FormattedMessage: typeof ReactIntl.FormattedMessage; defaultValue?: Mappings; areErrorsVisible?: boolean; } @@ -73,7 +73,6 @@ const deSerializer = (data: Record): Record => export const MappingsEditor = ({ setGetDataHandler, - FormattedMessage, areErrorsVisible = true, defaultValue, }: Props) => { @@ -84,20 +83,14 @@ export const MappingsEditor = ({ }, [form]); return ( -
      + {/* Global Mappings configuration */} @@ -111,7 +104,7 @@ export const MappingsEditor = ({

      Document fields

      - -
      + + ); }; From bf94053b457b7ed40bfb67b20f44d0b68db6480d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 30 Jul 2019 17:00:21 +0200 Subject: [PATCH 17/64] Use custom component to render the field name parameter --- .../mappings_editor/components/index.ts | 1 + .../components/parameters/index.ts | 23 ++++++++ .../components/parameters/name.tsx | 58 +++++++++++++++++++ .../components/properties_manager.tsx | 2 +- .../components/property_editor.tsx | 3 +- .../config/parameters_definition.ts | 25 ++++++++ .../components/mappings_editor/constants.ts | 4 ++ 7 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts index 56ee889992009..ad6189a87ad0e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts @@ -5,3 +5,4 @@ */ export * from './properties_manager'; +export { getComponentForParameter } from './parameters'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts new file mode 100644 index 0000000000000..6e596610ffece --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { ComponentType } from 'react'; + +import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { ParameterName } from '../../config'; + +import { Name } from './name'; + +type Comp = ({ field }: { field: FieldType } & any) => JSX.Element; + +const parameterMapToComponent: { [key in ParameterName]?: Comp } = { + name: Name, +}; + +export const getComponentForParameter = (parameter: ParameterName): Comp => { + return parameterMapToComponent[parameter] || Field; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx new file mode 100644 index 0000000000000..ea78cb05da467 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiFormRow, EuiFieldText } from '@elastic/eui'; + +import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; + +import { ERROR_CODES } from '../../constants'; + +interface Props { + field: FieldType; + fieldProps?: Record; +} + +export const Name = ({ field, fieldProps = {} }: Props) => { + // Errors for field + const errorMessageField = field.form.isSubmitted ? field.getErrorsMessages() : null; + + // Errors of name conflict + const errorMessageNameConflict = field.getErrorsMessages({ + errorCode: ERROR_CODES.NAME_CONFLICT, + }); + + const isInvalid = field.errors.length + ? field.form.isSubmitted || errorMessageNameConflict !== null + : false; + + // Concatenate error messages. + const errorMessage: string | null = + errorMessageField && errorMessageNameConflict + ? `${errorMessageField}, ${errorMessageNameConflict}` + : errorMessageField + ? errorMessageField + : errorMessageNameConflict; + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index bbdedcf6b62d4..4c0d157de5a67 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment } from 'react'; -import { EuiSpacer, EuiButton, EuiAccordion } from '@elastic/eui'; +import { EuiButton, EuiAccordion } from '@elastic/eui'; import uuid from 'uuid'; import { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 24fb010a94a82..ef6e44760fbd0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -16,6 +16,7 @@ import { parametersDefinition, dataTypesDefinition, DataType } from '../config'; import { hasNestedProperties } from '../helpers'; import { PropertyBasicParameters } from './property_basic_parameters'; import { PropertiesManager } from './properties_manager'; +import { getComponentForParameter } from './parameters'; interface Props { form: Form; @@ -67,7 +68,7 @@ export const PropertyEditor = ({ form={form} defaultValue={isEditMode ? undefined : ''} // "undefined" will into the "defaultValue" object passed to the form config={parametersDefinition.name.fieldConfig} - component={Field} + component={getComponentForParameter('name')} /> )} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 49714c5d5dea5..dd732db58c092 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -14,6 +14,8 @@ import { containsCharsField, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; +import { ERROR_CODES } from '../constants'; + export type ParameterName = | 'name' | 'type' @@ -51,6 +53,29 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { message: 'Spaces are not allowed in the name.', }), }, + { + validator: ({ path, value, formData }) => { + const regEx = /(.+)(\d+\.name)$/; + const regExResult = regEx.exec(path); + + if (regExResult) { + const { 1: parentPath } = regExResult; + // Get all the "name" properties on the parent path + const namePropertyPaths = Object.keys(formData).filter( + key => key !== path && key.startsWith(parentPath) && key.endsWith('name') + ); + + for (const namePath of namePropertyPaths) { + if (formData[namePath] === value) { + return { + code: ERROR_CODES.NAME_CONFLICT, + message: 'A field with the same name already exists.', + }; + } + } + } + }, + }, ], }, }, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts index 2343b82d6712a..9a38fd2252022 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts @@ -9,3 +9,7 @@ export const DYNAMIC_SETTING_OPTIONS = [ { value: false, text: 'false' }, { value: 'strict', text: 'strict' }, ]; + +export const ERROR_CODES = { + NAME_CONFLICT: 'ERR_NAME_CONCLICT', +}; From b2af5450869ec793bdc885542d0e014e216e181b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 30 Jul 2019 20:17:38 +0200 Subject: [PATCH 18/64] Validate name conflict inside component --- .../components/mappings_editor/_styles.scss | 4 +- .../components/parameters/name.tsx | 76 ++++++++++++++++--- .../components/properties_manager.tsx | 41 +++++----- .../config/parameters_definition.ts | 2 +- 4 files changed, 87 insertions(+), 36 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index 20453f00619cf..b27e6a4567ef7 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -19,14 +19,12 @@ .tree, .tree ul { - margin:0 0 0 1em; /* indentation */ + margin:0; padding:0; list-style:none; position:relative; } - .tree ul {margin-left:.5em} - .tree:before, .tree ul:before { content:""; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx index ea78cb05da467..7c537cf95cd91 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -7,7 +7,10 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { + Field as FieldType, + VALIDATION_TYPES, +} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { ERROR_CODES } from '../../constants'; @@ -17,25 +20,76 @@ interface Props { } export const Name = ({ field, fieldProps = {} }: Props) => { - // Errors for field - const errorMessageField = field.form.isSubmitted ? field.getErrorsMessages() : null; + const { form, errors } = field; + + const errorMessagesField = errors + .filter( + err => err.validationType === VALIDATION_TYPES.FIELD && err.code !== ERROR_CODES.NAME_CONFLICT + ) + .map(e => e.message as string) + .join(', '); // Errors of name conflict - const errorMessageNameConflict = field.getErrorsMessages({ + const errorMessagesNameConflict = field.getErrorsMessages({ errorCode: ERROR_CODES.NAME_CONFLICT, }); const isInvalid = field.errors.length - ? field.form.isSubmitted || errorMessageNameConflict !== null + ? form.isSubmitted || errorMessagesNameConflict !== null : false; // Concatenate error messages. const errorMessage: string | null = - errorMessageField && errorMessageNameConflict - ? `${errorMessageField}, ${errorMessageNameConflict}` - : errorMessageField - ? errorMessageField - : errorMessageNameConflict; + errorMessagesField && errorMessagesNameConflict + ? `${errorMessagesField}, ${errorMessagesNameConflict}` + : errorMessagesField + ? errorMessagesField + : errorMessagesNameConflict; + + const validateNameConflict = (value: string) => { + const formData = field.form.getFormData({ unflatten: false }); + const regEx = /(.+)(\d+\.name)$/; + const regExResult = regEx.exec(field.path); + + if (regExResult) { + const { 1: parentPath } = regExResult; + // Get all the "name" parameter of each property on the parent object + const namePropertyPaths = Object.keys(formData).filter( + key => key !== field.path && key.startsWith(parentPath) && key.endsWith('name') + ); + + // Remove any previous name conflict as we might have cleared it by changing this field name + for (const namePath of namePropertyPaths) { + const nameField = form.getFields()[namePath]; + const _errors = nameField.errors.filter( + err => err.code === ERROR_CODES.NAME_CONFLICT && err.existingPath === field.path + ); + + if (_errors.length < nameField.errors.length) { + form.setFieldErrors(namePath, _errors); + } + } + + // Check if the current value conflicts with other field name + for (const namePath of namePropertyPaths) { + if (value === formData[namePath]) { + form.setFieldErrors(field.path, [ + { + message: 'A field with the same name already exists.', + existingPath: namePath, + code: ERROR_CODES.NAME_CONFLICT, + }, + ]); + break; + } + } + } + }; + + const onNameChange = (e: React.ChangeEvent) => { + field.onChange(e); + validateNameConflict(e.target.value); + }; return ( { + Add property @@ -60,25 +61,23 @@ export const PropertiesManager = ({ ); return ( - - - {({ items, addItem, removeItem }) => { - if (depthLevel === 0) { - // At the root level we don't add the accordion - return renderPropertiesTree({ items, addItem, removeItem }); - } - return ( - - {renderPropertiesTree({ items, addItem, removeItem })} - - ); - }} - - + + {({ items, addItem, removeItem }) => { + if (depthLevel === 0) { + // At the root level we don't add the accordion + return renderPropertiesTree({ items, addItem, removeItem }); + } + return ( + + {renderPropertiesTree({ items, addItem, removeItem })} + + ); + }} + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index dd732db58c092..44418ba790d2e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -45,7 +45,7 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { defaultValue: '', validations: [ { - validator: emptyField('Please give a name to the property'), + validator: emptyField('Give a name to the field.'), }, { validator: containsCharsField({ From a65dd7799d9b035d6516da5ca99deb6d60ca7640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 31 Jul 2019 11:43:27 +0200 Subject: [PATCH 19/64] Update validation of property name --- .../components/parameters/index.ts | 12 ++-- .../components/parameters/name.tsx | 59 ++---------------- .../components/property_editor.tsx | 15 ++--- .../config/parameters_definition.ts | 62 ++++++++++++++++--- .../ui/components/mappings_editor/errors.ts | 19 ++++++ 5 files changed, 90 insertions(+), 77 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts index 6e596610ffece..dcdd744f307fd 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentType } from 'react'; - import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { ParameterName } from '../../config'; - import { Name } from './name'; -type Comp = ({ field }: { field: FieldType } & any) => JSX.Element; +type FieldComponent = ({ field }: { field: FieldType }) => JSX.Element; -const parameterMapToComponent: { [key in ParameterName]?: Comp } = { +const parameterMapToComponent: { [key in ParameterName]?: FieldComponent } = { name: Name, }; -export const getComponentForParameter = (parameter: ParameterName): Comp => { - return parameterMapToComponent[parameter] || Field; -}; +export const getComponentForParameter = (parameter: ParameterName): FieldComponent => + parameterMapToComponent[parameter] || Field; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx index 7c537cf95cd91..e19f4b76f1da2 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -7,10 +7,7 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { - Field as FieldType, - VALIDATION_TYPES, -} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { ERROR_CODES } from '../../constants'; @@ -22,14 +19,13 @@ interface Props { export const Name = ({ field, fieldProps = {} }: Props) => { const { form, errors } = field; + // All validation messages, except name conflict const errorMessagesField = errors - .filter( - err => err.validationType === VALIDATION_TYPES.FIELD && err.code !== ERROR_CODES.NAME_CONFLICT - ) + .filter(err => err.code !== ERROR_CODES.NAME_CONFLICT) .map(e => e.message as string) .join(', '); - // Errors of name conflict + // Name conflict error message const errorMessagesNameConflict = field.getErrorsMessages({ errorCode: ERROR_CODES.NAME_CONFLICT, }); @@ -46,51 +42,6 @@ export const Name = ({ field, fieldProps = {} }: Props) => { ? errorMessagesField : errorMessagesNameConflict; - const validateNameConflict = (value: string) => { - const formData = field.form.getFormData({ unflatten: false }); - const regEx = /(.+)(\d+\.name)$/; - const regExResult = regEx.exec(field.path); - - if (regExResult) { - const { 1: parentPath } = regExResult; - // Get all the "name" parameter of each property on the parent object - const namePropertyPaths = Object.keys(formData).filter( - key => key !== field.path && key.startsWith(parentPath) && key.endsWith('name') - ); - - // Remove any previous name conflict as we might have cleared it by changing this field name - for (const namePath of namePropertyPaths) { - const nameField = form.getFields()[namePath]; - const _errors = nameField.errors.filter( - err => err.code === ERROR_CODES.NAME_CONFLICT && err.existingPath === field.path - ); - - if (_errors.length < nameField.errors.length) { - form.setFieldErrors(namePath, _errors); - } - } - - // Check if the current value conflicts with other field name - for (const namePath of namePropertyPaths) { - if (value === formData[namePath]) { - form.setFieldErrors(field.path, [ - { - message: 'A field with the same name already exists.', - existingPath: namePath, - code: ERROR_CODES.NAME_CONFLICT, - }, - ]); - break; - } - } - } - }; - - const onNameChange = (e: React.ChangeEvent) => { - field.onChange(e); - validateNameConflict(e.target.value); - }; - return ( { + {formData => { - const fieldName = formData[`${fieldPathPrefix}name`] as string; const selectedDatatype = formData[`${fieldPathPrefix}type`] as DataType; const typeDefinition = dataTypesDefinition[selectedDatatype]; @@ -66,7 +62,7 @@ export const PropertyEditor = ({ @@ -144,7 +140,12 @@ export const PropertyEditor = ({
      )} - {renderNestedProperties(selectedDatatype, fieldName)} + + {_formData => { + const fieldName = _formData[`${fieldPathPrefix}name`] as string; + return renderNestedProperties(selectedDatatype, fieldName); + }} + ); }} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 44418ba790d2e..e0b1c81d765e4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -14,6 +14,7 @@ import { containsCharsField, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; +import { nameConflictError } from '../errors'; import { ERROR_CODES } from '../constants'; export type ParameterName = @@ -54,24 +55,69 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { }), }, { - validator: ({ path, value, formData }) => { + validator: ({ path, value, form, formData }) => { const regEx = /(.+)(\d+\.name)$/; const regExResult = regEx.exec(path); if (regExResult) { const { 1: parentPath } = regExResult; // Get all the "name" properties on the parent path - const namePropertyPaths = Object.keys(formData).filter( - key => key !== path && key.startsWith(parentPath) && key.endsWith('name') - ); + const namePropertyPaths = Object.keys(formData).filter(key => { + // Make sure we are filtering *only* the properties at the + // same nested object level + const isSameNestedLevel = Math.abs(key.length - path.length) <= 3; + + return ( + key !== path && + isSameNestedLevel && + key.startsWith(parentPath) && + key.endsWith('name') + ); + }); + + // Keep a referende of all the field name that have + // a conflict with the current field. + const conflictPaths: string[] = []; for (const namePath of namePropertyPaths) { + const nameField = form.getFields()[namePath]; + const nameFieldConflictError = nameField.errors.filter( + err => err.code === ERROR_CODES.NAME_CONFLICT + )[0]; + + let error; if (formData[namePath] === value) { - return { - code: ERROR_CODES.NAME_CONFLICT, - message: 'A field with the same name already exists.', - }; + conflictPaths.push(namePath); + if (!nameFieldConflictError) { + error = nameConflictError([path]); + } else { + error = nameConflictError([...nameFieldConflictError.conflictPaths, path]); + } + } else if ( + nameFieldConflictError && + (nameFieldConflictError.conflictPaths as string[]).some( + conflictPath => conflictPath === path + ) + ) { + if ((nameFieldConflictError.conflictPaths as string[]).length > 1) { + const updatedConflictPaths = (nameFieldConflictError.conflictPaths as string[]).filter( + p => p !== path + ); + error = nameConflictError(updatedConflictPaths); + } else { + nameField.setErrors([]); + } } + + if (error) { + // Update the validation on the other field + nameField.setErrors([error]); + } + } + + if (conflictPaths.length) { + // Update the validation on the current field being validated + return nameConflictError(conflictPaths); } } }, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts new file mode 100644 index 0000000000000..02c551bcbf5f9 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts @@ -0,0 +1,19 @@ +/* + * 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 { ValidationError } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; + +import { ERROR_CODES } from './constants'; + +/** + * Error creators + */ + +export const nameConflictError = (conflictPaths: string[]): ValidationError => ({ + code: ERROR_CODES.NAME_CONFLICT, + message: 'A field with the same name already exists.', + conflictPaths, +}); From 11b7987745688a1fa9fc4f3179f9735fc3e9b3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 1 Aug 2019 16:46:19 +0200 Subject: [PATCH 20/64] Add text and keyword child field support --- .../components/properties_manager.tsx | 75 ++++++---- .../components/property_editor.tsx | 128 ++++++++++++------ .../ui/components/mappings_editor/helpers.ts | 33 +++-- .../mappings_editor/mappings_editor.tsx | 2 +- 4 files changed, 163 insertions(+), 75 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index ee9137ef10a02..9ccfb4555323e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -18,17 +18,28 @@ import { PropertyEditor } from './property_editor'; interface Props { form: Form; - depthLevel?: number; + parentType?: string; path?: string; fieldName?: string; } -export const PropertiesManager = ({ - form, - depthLevel, - path = 'properties', - fieldName = '', -}: Props) => { +export const PropertiesManager = ({ form, parentType = 'root', path, fieldName = '' }: Props) => { + const getArrayPath = () => { + if (parentType === 'root') { + return 'properties'; + } + + return parentType === 'text' || parentType === 'keyword' + ? `${path}fields` + : `${path}properties`; + }; + + const getAccordionButtonContent = () => { + return parentType === 'text' || parentType === 'keyword' + ? `${fieldName} child fields` + : `${fieldName} properties`; + }; + const renderPropertiesTree = ({ items, addItem, @@ -39,38 +50,48 @@ export const PropertiesManager = ({ removeItem: (id: number) => void; }) => ( -
        - {items.map(({ id, path: itemPath, isNew }) => { - return ( -
      • - removeItem(id)} - isEditMode={!isNew} - /> -
      • - ); - })} -
      - + {items.length > 0 && ( + +
        + {items.map(({ id, path: itemPath, isNew }) => { + return ( +
      • + removeItem(id)} + isEditMode={!isNew} + /> +
      • + ); + })} +
      + +
      + )} + - Add property + {parentType === 'text' || parentType === 'keyword' ? 'Add child field' : 'Add property'}
      ); return ( - + {({ items, addItem, removeItem }) => { - if (depthLevel === 0) { - // At the root level we don't add the accordion + if (parentType === 'root' || items.length === 0) { + // At the root level or if there aren't any property + // we don't add the accordion return renderPropertiesTree({ items, addItem, removeItem }); } return ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 7c938dd4aad24..76c6fecd51b8a 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -3,8 +3,16 @@ * 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 } from 'react'; -import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButtonIcon, + EuiFormRow, + EuiSelect, +} from '@elastic/eui'; import { UseField, Form, @@ -12,18 +20,18 @@ import { } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; -import { parametersDefinition, dataTypesDefinition, DataType } from '../config'; +import { parametersDefinition, dataTypesDefinition, DataType, SubType } from '../config'; import { hasNestedProperties } from '../helpers'; import { PropertyBasicParameters } from './property_basic_parameters'; import { PropertiesManager } from './properties_manager'; import { getComponentForParameter } from './parameters'; +import { getAdvancedSettingsCompForType } from './advanced_settings'; interface Props { form: Form; onRemove?: () => void; fieldPathPrefix?: string; isDeletable?: boolean; - isAnonymous?: boolean; isEditMode?: boolean; } @@ -31,22 +39,41 @@ export const PropertyEditor = ({ form, fieldPathPrefix = '', onRemove = () => undefined, - isAnonymous = false, isDeletable = true, isEditMode = false, }: Props) => { + const [isAdvancedSettingsVisible, setIsAdvancedSettingsVisible] = useState(false); const renderNestedProperties = (selectedType: DataType, fieldName: string) => hasNestedProperties(selectedType) ? ( ) : null; + const toggleAdvancedSettings = () => { + setIsAdvancedSettingsVisible(previous => !previous); + }; + + const renderAdvancedSettings = (type: DataType | SubType) => { + const AdvancedSettingsComponent = getAdvancedSettingsCompForType(type); + + if (!isAdvancedSettingsVisible || !AdvancedSettingsComponent) { + return null; + } + return ( + + + + + ); + }; + return ( {formData => { @@ -57,17 +84,16 @@ export const PropertyEditor = ({
      {/* Field name */} - {isAnonymous !== true && ( - - - - )} + + + + {/* Field type */} ({ - value, - text: label, - })), - }, - }} - /> + > + {field => ( + + { + setIsAdvancedSettingsVisible(false); + field.setValue(e.target.value); + }} + hasNoInitialSelection={true} + isInvalid={false} + options={Object.entries(dataTypesDefinition).map(([value, { label }]) => ({ + value, + text: label, + }))} + /> + + )} + + {/* Field sub type (if any) */} {typeDefinition && typeDefinition.subTypes && ( @@ -126,24 +163,39 @@ export const PropertyEditor = ({ )} - {typeDefinition && typeDefinition.basicParameters && ( + {((typeDefinition && typeDefinition.basicParameters) || + getAdvancedSettingsCompForType(selectedDatatype)) && ( - - {/* Basic parameters for the selected type */} - + + {typeDefinition && typeDefinition.basicParameters && ( + + {/* Basic parameters for the selected type */} + + + )} + {getAdvancedSettingsCompForType(selectedDatatype) && ( + + + {isAdvancedSettingsVisible ? 'Hide' : 'Show'} advanced settings + + + )} + )} + {renderAdvancedSettings(selectedDatatype)} + {_formData => { - const fieldName = _formData[`${fieldPathPrefix}name`] as string; - return renderNestedProperties(selectedDatatype, fieldName); + const nameValue = _formData[`${fieldPathPrefix}name`] as string; + return renderNestedProperties(selectedDatatype, nameValue); }}
      diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts index 9949913d511f4..57d9d13201529 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts @@ -6,16 +6,23 @@ import { DataType } from './config'; export const hasNestedProperties = (selectedDatatype: DataType) => - selectedDatatype === 'object' || selectedDatatype === 'nested'; + selectedDatatype === 'object' || + selectedDatatype === 'nested' || + selectedDatatype === 'text' || + selectedDatatype === 'keyword'; export const propertiesArrayToObject = (properties?: any[]): any => properties ? properties.reduce((acc, property) => { const { name, ...rest } = property; acc[property.name] = rest; - if (hasNestedProperties(rest.type) && rest.properties) { + if (hasNestedProperties(rest.type)) { // Recursively convert Array to Object - rest.properties = propertiesArrayToObject(rest.properties); + if (rest.properties) { + rest.properties = propertiesArrayToObject(rest.properties); + } else if (rest.fields) { + rest.fields = propertiesArrayToObject(rest.fields); + } } return acc; }, {}) @@ -27,13 +34,21 @@ export const propertiesObjectToArray = ( } = {} ): any[] => Object.entries(properties).map(([name, property]) => { - if (hasNestedProperties(property.type) && property.properties) { + if (hasNestedProperties(property.type)) { // Recursively convert Object to Array - return { - name, - ...property, - properties: propertiesObjectToArray(property.properties), - }; + if (property.properties) { + return { + name, + ...property, + properties: propertiesObjectToArray(property.properties), + }; + } else if (property.fields) { + return { + name, + ...property, + fields: propertiesObjectToArray(property.fields), + }; + } } return { name, ...property }; }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 04305e7365a01..9eb1a97025f6c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -104,7 +104,7 @@ export const MappingsEditor = ({

      Document fields

      - + ); }; From 80c64d3df6f61173a83515150b88a772f933f9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 1 Aug 2019 18:13:49 +0200 Subject: [PATCH 21/64] Add advanced settings for Text type --- .../components/advanced_settings/index.ts | 19 ++++ .../components/advanced_settings/keyword.tsx | 10 +++ .../components/advanced_settings/text.tsx | 87 +++++++++++++++++++ .../components/property_editor.tsx | 8 +- .../config/parameters_definition.ts | 34 +++++++- .../components/mappings_editor/constants.ts | 19 ++++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts new file mode 100644 index 0000000000000..f49b27c7c0b74 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { ComponentType } from 'react'; +import { DataType, SubType } from '../../config'; + +import { TextAdvancedSettings } from './text'; +import { KeywordAdvancedSettings } from './keyword'; + +const parameterMapToAdvancedSettingsComp: { [key in DataType | SubType]?: ComponentType } = { + text: TextAdvancedSettings, + keyword: KeywordAdvancedSettings, +}; + +export const getAdvancedSettingsCompForType = ( + type: DataType | SubType +): ComponentType | undefined => parameterMapToAdvancedSettingsComp[type]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx new file mode 100644 index 0000000000000..13a4f559411ff --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx @@ -0,0 +1,10 @@ +/* + * 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'; + +export const KeywordAdvancedSettings = () => { + return
      KEYWORD ADVANCED SETTINGS
      ; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx new file mode 100644 index 0000000000000..ead9e3d50d150 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + UseField, + Form, +} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; + +import { parametersDefinition } from '../../config'; +import { ANALYZERS_OPTIONS, INDEX_OPTIONS } from '../../constants'; + +interface Props { + fieldPathPrefix: string; + isEditMode: boolean; + form: Form; +} + +export const TextAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Props) => { + return ( +
      + + + + + + + + + + + + + + +
      + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx index 76c6fecd51b8a..b2b941400c258 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx @@ -69,7 +69,13 @@ export const PropertyEditor = ({ return ( - +
      + +
      ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index e0b1c81d765e4..931820da5bbad 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -31,7 +31,11 @@ export type ParameterName = | 'enabled' | 'boost' | 'locale' - | 'format'; + | 'format' + | 'analyzer' + | 'search_analyzer' + | 'search_quote_analyzer' + | 'index_options'; export interface Parameter { fieldConfig?: FieldConfig; @@ -222,4 +226,32 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { Array.isArray(formats) ? formats : (formats as string).split('||'), }, }, + analyzer: { + fieldConfig: { + label: 'Analyzer', + defaultValue: 'text', + type: FIELD_TYPES.SELECT, + }, + }, + search_analyzer: { + fieldConfig: { + label: 'Search analyzer', + defaultValue: 'text', + type: FIELD_TYPES.SELECT, + }, + }, + search_quote_analyzer: { + fieldConfig: { + label: 'Search quote analyzer', + defaultValue: 'text', + type: FIELD_TYPES.SELECT, + }, + }, + index_options: { + fieldConfig: { + label: 'Index options', + defaultValue: 'docs', + type: FIELD_TYPES.SELECT, + }, + }, }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts index 9a38fd2252022..fe54cf0f99e13 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts @@ -10,6 +10,25 @@ export const DYNAMIC_SETTING_OPTIONS = [ { value: 'strict', text: 'strict' }, ]; +export const ANALYZERS_OPTIONS = [ + { value: 'index_default', text: 'Index default' }, + { value: 'standard', text: 'Standard' }, + { value: 'simple', text: 'Simple' }, + { value: 'whitespace', text: 'Whitespace' }, + { value: 'stop', text: 'Stop' }, + { value: 'keyword', text: 'Keyword' }, + { value: 'pattern', text: 'Pattern' }, + { value: 'language', text: 'Language' }, + { value: 'fingerprint', text: 'Fingerprint' }, +]; + +export const INDEX_OPTIONS = [ + { value: 'docs', text: 'Docs' }, + { value: 'freqs', text: 'Freqs' }, + { value: 'positions', text: 'Positions' }, + { value: 'offsets', text: 'Offsets' }, +]; + export const ERROR_CODES = { NAME_CONFLICT: 'ERR_NAME_CONCLICT', }; From 344344aa810a4170e8a0014b63f7c5f6a080acd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 2 Aug 2019 12:06:45 +0200 Subject: [PATCH 22/64] Add all advanced settings for text type --- .../components/advanced_settings/text.tsx | 243 +++++++++++++----- .../components/properties_manager.tsx | 2 +- .../components/property_basic_parameters.tsx | 22 +- .../components/property_editor.tsx | 25 +- .../config/parameters_definition.ts | 79 +++++- .../components/mappings_editor/constants.ts | 6 + 6 files changed, 288 insertions(+), 89 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx index ead9e3d50d150..a531822ee7c4b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx @@ -3,17 +3,18 @@ * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { UseField, Form, + FieldConfig, } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; -import { parametersDefinition } from '../../config'; -import { ANALYZERS_OPTIONS, INDEX_OPTIONS } from '../../constants'; +import { parametersDefinition, ParameterName } from '../../config'; +import { ANALYZERS_OPTIONS, INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; interface Props { fieldPathPrefix: string; @@ -21,67 +22,181 @@ interface Props { form: Form; } +const fieldConfig = (param: ParameterName): FieldConfig => + parametersDefinition[param].fieldConfig || {}; + +const defaultValueParam = (param: ParameterName): unknown => + typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; + export const TextAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Props) => { return ( -
      - - - - - - - - - - - - - - -
      + +
      + + + + + + + + + + + + + + +
      +
      + + + + + + + + + + + + + + +
      +
      + + + + + + + + + + + + + + + + + + + + + + + +
      +
      ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx index 9ccfb4555323e..d17049ddee4c5 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx @@ -52,7 +52,7 @@ export const PropertiesManager = ({ form, parentType = 'root', path, fieldName = {items.length > 0 && ( -
        +
          {items.map(({ id, path: itemPath, isNew }) => { return (
        • diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx index 539ed26c36f4c..5130feb42ab7b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx @@ -24,6 +24,15 @@ interface Props { const parametersToRows = (params: ParameterName[] | ParameterName[][]): ParameterName[][] => Array.isArray(params[0]) ? (params as ParameterName[][]) : ([params] as ParameterName[][]); +// If we have 2 or less items to display, we limit the width +// of the container to limit the size of the . +const getMaxWidth = (rowIndex: number, totalItems: number) => { + if (rowIndex === 0 || totalItems >= 3) { + return 'initial'; + } + return totalItems <= 1 ? '300px' : '600px'; +}; + export const PropertyBasicParameters = ({ form, typeDefinition, @@ -36,16 +45,7 @@ export const PropertyBasicParameters = ({ const rows = parametersToRows(typeDefinition.basicParameters); - // If we have 2 or less items to display, we limit the width - // of the container to limit the size of the . - const getMaxWidth = (rowIndex: number, totalItems: number) => { - if (rowIndex === 0 || totalItems >= 3) { - return 'initial'; - } - return totalItems <= 1 ? '300px' : '600px'; - }; - - const getDefaultValue = (parameter: ParameterName): unknown | undefined => + const defaultValueParam = (parameter: ParameterName): unknown | undefined => isEditMode ? undefined : parametersDefinition[parameter] && @@ -63,7 +63,7 @@ export const PropertyBasicParameters = ({ + parametersDefinition[param].fieldConfig || {}; + +const defaultValueParam = (param: ParameterName): unknown => + typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; + export const PropertyEditor = ({ form, fieldPathPrefix = '', @@ -94,8 +107,8 @@ export const PropertyEditor = ({ @@ -105,8 +118,8 @@ export const PropertyEditor = ({ {field => ( @@ -137,7 +150,7 @@ export const PropertyEditor = ({ form={form} defaultValue={isEditMode ? undefined : typeDefinition.subTypes.types[0]} config={{ - ...parametersDefinition.type.fieldConfig, + ...fieldConfig('type'), label: typeDefinition.subTypes.label, }} component={Field} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 931820da5bbad..b46dae71d3466 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -35,15 +35,24 @@ export type ParameterName = | 'analyzer' | 'search_analyzer' | 'search_quote_analyzer' - | 'index_options'; + | 'index_options' + | 'eager_global_ordinals' + | 'index_prefixes' + | 'index_phrases' + | 'norms' + | 'term_vector' + | 'position_increment_gap' + | 'similarity'; export interface Parameter { - fieldConfig?: FieldConfig; + fieldConfig?: FieldConfig | Record; paramName?: string; docs?: string; } -export const parametersDefinition: { [key in ParameterName]: Parameter } = { +export const parametersDefinition: { + [key in ParameterName]: Parameter; +} = { name: { fieldConfig: { label: 'Field name', @@ -163,7 +172,7 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { fieldConfig: { label: 'Fielddata', type: FIELD_TYPES.CHECKBOX, - defaultValue: true, + defaultValue: false, }, docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', }, @@ -229,21 +238,21 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { analyzer: { fieldConfig: { label: 'Analyzer', - defaultValue: 'text', + defaultValue: 'index_default', type: FIELD_TYPES.SELECT, }, }, search_analyzer: { fieldConfig: { label: 'Search analyzer', - defaultValue: 'text', + defaultValue: 'index_default', type: FIELD_TYPES.SELECT, }, }, search_quote_analyzer: { fieldConfig: { label: 'Search quote analyzer', - defaultValue: 'text', + defaultValue: 'index_default', type: FIELD_TYPES.SELECT, }, }, @@ -254,4 +263,60 @@ export const parametersDefinition: { [key in ParameterName]: Parameter } = { type: FIELD_TYPES.SELECT, }, }, + eager_global_ordinals: { + fieldConfig: { + label: 'Eager global ordinals', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + index_phrases: { + fieldConfig: { + label: 'Index phrases', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + norms: { + fieldConfig: { + label: 'Norms', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + term_vector: { + fieldConfig: { + label: 'Term vectors', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + position_increment_gap: { + fieldConfig: { + label: 'Position increment gap', + type: FIELD_TYPES.NUMBER, + defaultValue: 100, + }, + }, + index_prefixes: { + fieldConfig: { + min: { + type: FIELD_TYPES.NUMBER, + defaultValue: 2, + helpText: 'Min.', + }, + max: { + type: FIELD_TYPES.NUMBER, + defaultValue: 5, + helpText: 'Max.', + }, + }, + }, + similarity: { + fieldConfig: { + label: 'Similarity algorithm', + defaultValue: 'BM25', + type: FIELD_TYPES.SELECT, + }, + }, }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts index fe54cf0f99e13..ecb0c0f10ce4c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts @@ -29,6 +29,12 @@ export const INDEX_OPTIONS = [ { value: 'offsets', text: 'Offsets' }, ]; +export const SIMILARITY_ALGORITHM_OPTIONS = [ + { value: 'BM25', text: 'BM25' }, + { value: 'classic', text: 'classic' }, + { value: 'boolean', text: 'boolean' }, +]; + export const ERROR_CODES = { NAME_CONFLICT: 'ERR_NAME_CONCLICT', }; From 6f91b24d84b230199ecaebf5729bcd56c45bf758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 2 Aug 2019 12:51:23 +0200 Subject: [PATCH 23/64] Add validation to advanced text settings --- .../components/advanced_settings/text.tsx | 30 ++++-- .../config/parameters_definition.ts | 92 +++++++++++++++++-- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx index a531822ee7c4b..1c03040543d72 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx @@ -147,27 +147,41 @@ export const TextAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Prop @@ -191,7 +205,9 @@ export const TextAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Prop config={fieldConfig('similarity')} component={Field} componentProps={{ - fieldProps: { options: SIMILARITY_ALGORITHM_OPTIONS }, + fieldProps: { + options: SIMILARITY_ALGORITHM_OPTIONS, + }, }} /> diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index b46dae71d3466..1c804d65e1f54 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -14,6 +14,8 @@ import { containsCharsField, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; +import { toInt } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib'; + import { nameConflictError } from '../errors'; import { ERROR_CODES } from '../constants'; @@ -200,8 +202,9 @@ export const parametersDefinition: { boost: { fieldConfig: { label: 'Boost', - defaultValue: '', - type: FIELD_TYPES.TEXT, + defaultValue: 1.0, + type: FIELD_TYPES.NUMBER, + formatters: [toInt], }, }, dynamic: { @@ -296,19 +299,96 @@ export const parametersDefinition: { label: 'Position increment gap', type: FIELD_TYPES.NUMBER, defaultValue: 100, + formatters: [toInt], + validations: [ + { + validator: emptyField('Set a position increment gap value.'), + }, + { + validator: ({ value }) => { + if ((value as number) < 0) { + return { + message: 'The value must be greater or equal to 0.', + }; + } + }, + }, + ], }, }, index_prefixes: { fieldConfig: { - min: { + min_chars: { type: FIELD_TYPES.NUMBER, defaultValue: 2, - helpText: 'Min.', + helpText: 'Min chars.', + formatters: [toInt], + validations: [ + { + validator: emptyField('Set a min value.'), + }, + { + validator: ({ value }) => { + if ((value as number) < 0) { + return { + message: 'The value must be greater or equal than zero.', + }; + } + }, + }, + { + validator: ({ value, path, formData }) => { + const maxPath = path.replace('.min', '.max'); + const maxValue = formData[maxPath]; + + if ((maxValue as string) === '') { + return; + } + + if ((value as number) >= (maxValue as number)) { + return { + message: 'The value must be smaller than the max value.', + }; + } + }, + }, + ], }, - max: { + max_chars: { type: FIELD_TYPES.NUMBER, defaultValue: 5, - helpText: 'Max.', + helpText: 'Max chars.', + formatters: [toInt], + validations: [ + { + validator: emptyField('Set a max value.'), + }, + { + validator: ({ value }) => { + if ((value as number) > 20) { + return { + message: 'The value must be smaller or equal than 20.', + }; + } + }, + }, + { + validator: ({ value, path, formData }) => { + const minPath = path.replace('.max', '.min'); + const minValue = formData[minPath]; + + if ((minValue as string) === '') { + return; + } + + if ((value as number) <= (minValue as number)) { + return { + message: 'The value must be greater than the min value.', + }; + } + }, + }, + ], }, }, }, From 50f153f6b4e8d00e9834f29251fba4614409d2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 2 Aug 2019 13:31:21 +0200 Subject: [PATCH 24/64] Add advanced settings for "keyword" --- .../components/advanced_settings/keyword.tsx | 146 +++++++++++++++++- .../config/parameters_definition.ts | 35 ++++- 2 files changed, 177 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx index 13a4f559411ff..f9b8e3a6e5e79 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx @@ -3,8 +3,148 @@ * 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 React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -export const KeywordAdvancedSettings = () => { - return
          KEYWORD ADVANCED SETTINGS
          ; +import { + UseField, + Form, + FieldConfig, +} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; + +import { parametersDefinition, ParameterName } from '../../config'; +import { ANALYZERS_OPTIONS, INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; + +interface Props { + fieldPathPrefix: string; + isEditMode: boolean; + form: Form; +} + +const fieldConfig = (param: ParameterName): FieldConfig => + parametersDefinition[param].fieldConfig || {}; + +const defaultValueParam = (param: ParameterName): unknown => + typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; + +export const KeywordAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Props) => { + return ( + +
          + + + + + + + + + + + + + + +
          +
          + + + + + + + + +
          +
          + + + + + + + + + + + + + + +
          +
          + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 1c804d65e1f54..c79936682baeb 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -44,7 +44,10 @@ export type ParameterName = | 'norms' | 'term_vector' | 'position_increment_gap' - | 'similarity'; + | 'similarity' + | 'normalizer' + | 'ignore_above' + | 'split_queries_on_whitespace'; export interface Parameter { fieldConfig?: FieldConfig | Record; @@ -259,6 +262,21 @@ export const parametersDefinition: { type: FIELD_TYPES.SELECT, }, }, + normalizer: { + fieldConfig: { + label: 'Normalizer', + defaultValue: '', + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: containsCharsField({ + chars: ' ', + message: 'Spaces are not allowed.', + }), + }, + ], + }, + }, index_options: { fieldConfig: { label: 'Index options', @@ -399,4 +417,19 @@ export const parametersDefinition: { type: FIELD_TYPES.SELECT, }, }, + split_queries_on_whitespace: { + fieldConfig: { + label: 'Split queries on whitespace', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + ignore_above: { + fieldConfig: { + label: 'Ignore above', + defaultValue: 2147483647, + type: FIELD_TYPES.NUMBER, + formatters: [toInt], + }, + }, }; From ad8f7398f093419efed0ea23c03fcb0d3766e802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 2 Aug 2019 13:35:09 +0200 Subject: [PATCH 25/64] Add validation for "boost" and."ignore_above" --- .../config/parameters_definition.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index c79936682baeb..d5141f3a71705 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -208,6 +208,17 @@ export const parametersDefinition: { defaultValue: 1.0, type: FIELD_TYPES.NUMBER, formatters: [toInt], + validations: [ + { + validator: ({ value }) => { + if ((value as number) < 0) { + return { + message: 'The value must be greater or equal to 0.', + }; + } + }, + }, + ], }, }, dynamic: { @@ -430,6 +441,17 @@ export const parametersDefinition: { defaultValue: 2147483647, type: FIELD_TYPES.NUMBER, formatters: [toInt], + validations: [ + { + validator: ({ value }) => { + if ((value as number) < 0) { + return { + message: 'The value must be greater or equal to 0.', + }; + } + }, + }, + ], }, }, }; From ea546eedebf63afb5353991643526f37492b56fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 2 Aug 2019 13:42:40 +0200 Subject: [PATCH 26/64] Sanitize property parameter before outputting form --- .../mappings_editor/mappings_editor.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 9eb1a97025f6c..f4408d4d2c3d4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -31,6 +31,18 @@ export interface Mappings { [key: string]: any; } +const sanitizePropParameters = (parameters: Record): Record => + Object.entries(parameters).reduce( + (acc, [param, value]) => { + // IF a prop value is "index_default", we remove it + if (value !== 'index_default') { + acc[param] = value; + } + return acc; + }, + {} as any + ); + const serializeProperties = (properties: any[]) => properties.map(prop => { // If a subType is present, use it as type for ES @@ -38,7 +50,8 @@ const serializeProperties = (properties: any[]) => prop.type = prop.subType; delete prop.subType; } - return prop; + + return sanitizePropParameters(prop); }); const deSerializeProperties = (properties: { [key: string]: any }) => { From 08cb96183d98d0b3680b353aa44a06b317a9c8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 2 Aug 2019 13:55:28 +0200 Subject: [PATCH 27/64] Add "ip", "rank_feature", "dense_vector", "sparse_vector" types --- .../config/data_types_definition.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts index 41ba2c89a3cec..b8ef888c35b69 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts @@ -15,7 +15,12 @@ export type DataType = | 'boolean' | 'range' | 'object' - | 'nested'; + | 'nested' + | 'ip' + | 'rank_feature' + | 'rank_features' + | 'dense_vector' + | 'sparse_vector'; export type SubType = NumericType | DateType | RangeType; @@ -82,6 +87,10 @@ export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { label: 'Binary', basicParameters: ['doc_values', 'store'], }, + ip: { + label: 'IP', + basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], + }, boolean: { label: 'Boolean', basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], @@ -102,6 +111,18 @@ export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { label: 'Nested', basicParameters: ['dynamic'], }, + rank_feature: { + label: 'Rank feature', + }, + rank_features: { + label: 'Rank features', + }, + dense_vector: { + label: 'Dense vector', + }, + sparse_vector: { + label: 'Sparse vector', + }, }; const subTypesMapToType = Object.entries(dataTypesDefinition).reduce( From eb5a8674c796c0245a1ef3d196195605a357f8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Sun, 4 Aug 2019 14:34:52 +0200 Subject: [PATCH 28/64] Fix text copy --- .../mappings_editor/config/parameters_definition.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index d5141f3a71705..f135447165fe4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -213,7 +213,7 @@ export const parametersDefinition: { validator: ({ value }) => { if ((value as number) < 0) { return { - message: 'The value must be greater or equal to 0.', + message: 'The value must be greater or equal than 0.', }; } }, @@ -337,7 +337,7 @@ export const parametersDefinition: { validator: ({ value }) => { if ((value as number) < 0) { return { - message: 'The value must be greater or equal to 0.', + message: 'The value must be greater or equal than 0.', }; } }, @@ -446,7 +446,7 @@ export const parametersDefinition: { validator: ({ value }) => { if ((value as number) < 0) { return { - message: 'The value must be greater or equal to 0.', + message: 'The value must be greater or equal than 0.', }; } }, From b9fed7e0ab96b94087e841fd1feb753d0e09599d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 9 Aug 2019 16:33:06 +0200 Subject: [PATCH 29/64] Update UX with Readonly mode of Tree component (#12) --- .../components/mappings_editor/_styles.scss | 49 +++-- .../components/advanced_settings/keyword.tsx | 28 +-- .../components/advanced_settings/text.tsx | 37 ++-- .../components/configuration_form.tsx | 58 +++++ .../components/document_fields.tsx | 86 ++++++++ .../mappings_editor/components/index.ts | 6 +- .../components/parameters/name.tsx | 9 +- .../components/properties_contex.tsx | 103 +++++++++ .../components/properties_manager.tsx | 104 --------- .../property/delete_property_provider.tsx | 92 ++++++++ .../components/property/index.ts | 10 + .../{ => property}/property_editor.tsx | 198 ++++++++++++------ .../property/property_list_item.tsx | 146 +++++++++++++ .../components/property/property_view.tsx | 50 +++++ .../property/save_property_provider.tsx | 176 ++++++++++++++++ .../mappings_editor/components/tree/index.ts | 8 + .../mappings_editor/components/tree/tree.tsx | 67 ++++++ .../components/tree/tree_item.tsx | 15 ++ .../config/parameters_definition.ts | 71 ------- .../ui/components/mappings_editor/errors.ts | 3 +- .../ui/components/mappings_editor/helpers.ts | 100 +++++---- .../mappings_editor/mappings_editor.tsx | 155 ++++++-------- 22 files changed, 1133 insertions(+), 438 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/{ => property}/property_editor.tsx (53%) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index b27e6a4567ef7..f13d430809652 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -1,22 +1,32 @@ .mappings-editor { - .euiAccordion__childWrapper { - transition: none; - } - - .euiAccordion__padding--l { - padding-right: 0; - } + $gutter: 12px; + $border-tree: 1px dotted #aaa; .property-editor { - border-bottom: 1px solid #eee; - margin: -12px; - padding: 12px; + background-color: #fafbfd; + box-shadow: inset 0px 1px 3px 1px rgba(0,0,0,0.2); + margin-top: $gutter; + padding: $gutter; + position: relative; + padding-bottom: $gutter; + z-index: 1; - &:hover { - background-color: #fafbfd; + .action-buttons { + position: absolute; + top: -38px; + right: 0; } } + .property-list-item__overlay { + position: absolute; + top: 0; + left: 1px; + right: 0; + height: 48px; + background-color: rgba(255, 255, 255, 0.65); + } + .tree, .tree ul { margin:0; @@ -34,15 +44,15 @@ top:0; bottom:0; left:0; - border-left:1px dotted #aaa; + border-left: $border-tree; } .tree li { margin:0; - padding-left: 1.5em; + padding-left: $gutter * 2; line-height:2em; position:relative; - margin-bottom: 12px; + margin-bottom: $gutter; &:last-child { .property-editor { @@ -56,16 +66,17 @@ display:block; height:0; border-top:1px dotted #aaa; - margin-top:-1px; position:absolute; - top: 64px; - left:0; + top: 24px; + left: 1px; + width: $gutter; } .tree li:last-child:before { background:white; height:auto; - top: 64px; + top: $gutter * 2; bottom:0; + left: 0; } } diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx index f9b8e3a6e5e79..f841dea3eba97 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { UseField, @@ -14,10 +14,10 @@ import { import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; import { parametersDefinition, ParameterName } from '../../config'; -import { ANALYZERS_OPTIONS, INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; +import { INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; interface Props { - fieldPathPrefix: string; + // fieldPathPrefix: string; isEditMode: boolean; form: Form; } @@ -28,14 +28,14 @@ const fieldConfig = (param: ParameterName): FieldConfig => const defaultValueParam = (param: ParameterName): unknown => typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; -export const KeywordAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Props) => { +export const KeywordAdvancedSettings = ({ form, isEditMode }: Props) => { return (
          const defaultValueParam = (param: ParameterName): unknown => typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; -export const TextAdvancedSettings = ({ fieldPathPrefix, form, isEditMode }: Props) => { +export const TextAdvancedSettings = ({ form, isEditMode }: Props) => { return (
          Promise<{ isValid: boolean; data: any }>) => void; + onFormValidChange: (isValid: boolean) => void; + defaultValue?: any; +} + +export const ConfigurationForm = ({ + setGetDataHandler, + defaultValue, + onFormValidChange, +}: Props) => { + const { form } = useForm({ schema, defaultValue }); + + useEffect(() => { + setGetDataHandler(form.onSubmit); + onFormValidChange(form.isValid); + }, [form]); + + return ( + + {/* Global Mappings configuration */} + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx new file mode 100644 index 0000000000000..44c0f856b6ebb --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx @@ -0,0 +1,86 @@ +/* + * 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, { Fragment, useEffect } from 'react'; +import { EuiTitle, EuiSpacer, EuiButton } from '@elastic/eui'; + +import { Tree, TreeItem } from './tree'; +import { PropertyListItem, PropertyEditor, SavePropertyProvider } from './property'; +import { usePropertiesState, usePropertiesDispatch } from './properties_contex'; + +export interface DocumentFieldsState { + isEditing: boolean; + properties: Record; +} + +interface Props { + onUpdate: (state: DocumentFieldsState) => void; +} + +export const DocumentFields = ({ onUpdate }: Props) => { + const { properties, selectedPath, selectedObjectToAddProperty } = usePropertiesState(); + const dispatch = usePropertiesDispatch(); + + const showCreateForm = selectedObjectToAddProperty === ''; + + useEffect(() => { + onUpdate({ + properties, + isEditing: selectedPath !== null || selectedObjectToAddProperty !== null, + }); + }, [properties, selectedPath, selectedObjectToAddProperty]); + + const renderCreateForm = (style = {}) => ( + + {saveProperty => ( + ) => { + saveProperty({ newProperty, path: '', isEditMode: false, isCreateMode: true }); + }} + onCancel={() => dispatch({ type: 'selectObjectToAddProperty', value: null })} + parentObject={properties} + /> + )} + + ); + + return ( + + +

          Document fields

          +
          + + + + {Object.entries(properties) + // Make sure to display the fields in alphabetical order + .sort(([a], [b]) => (a < b ? -1 : 1)) + .map(([name, property], i) => ( + + + + ))} + + + {showCreateForm ? ( + renderCreateForm() + ) : ( + dispatch!({ type: 'selectObjectToAddProperty', value: '' })} + isDisabled={selectedPath !== null || selectedObjectToAddProperty !== null} + > + Add property + + )} +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts index ad6189a87ad0e..dc065cb5d2f3a 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts @@ -4,5 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './properties_manager'; +export * from './tree'; +export * from './property'; +export * from './configuration_form'; +export * from './document_fields'; +export * from './properties_contex'; export { getComponentForParameter } from './parameters'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx index e19f4b76f1da2..2423520017347 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -13,11 +13,12 @@ import { ERROR_CODES } from '../../constants'; interface Props { field: FieldType; + parentObject: Record; fieldProps?: Record; } -export const Name = ({ field, fieldProps = {} }: Props) => { - const { form, errors } = field; +export const Name = ({ field, parentObject, fieldProps = {} }: Props) => { + const { errors } = field; // All validation messages, except name conflict const errorMessagesField = errors @@ -30,9 +31,7 @@ export const Name = ({ field, fieldProps = {} }: Props) => { errorCode: ERROR_CODES.NAME_CONFLICT, }); - const isInvalid = field.errors.length - ? form.isSubmitted || errorMessagesNameConflict !== null - : false; + const isInvalid = field.errors.length ? !field.isUpdating : false; // Concatenate error messages. const errorMessage: string | null = diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx new file mode 100644 index 0000000000000..6218a27a4e14b --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx @@ -0,0 +1,103 @@ +/* + * 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, { useReducer, useContext } from 'react'; +import { get, set } from 'lodash'; + +import { unset } from '../helpers'; + +interface State { + properties: Record; + selectedPath: string | null; + selectedObjectToAddProperty: string | null; +} + +type Action = + | { type: 'selectPath'; value: string | null } + | { type: 'selectObjectToAddProperty'; value: string | null } + | { type: 'saveProperty'; path: string; value: Record } + | { type: 'deleteProperty'; path: string } + | { type: 'updatePropertyPath'; oldPath: string; newPath: string }; + +type Dispatch = (action: Action) => void; + +const PropertiesStateContext = React.createContext(undefined); +const PropertiesDispatchContext = React.createContext(undefined); + +function propertiesReducer(state: State, action: Action): State { + let updatedProperties: Record; + + switch (action.type) { + case 'selectPath': + return { ...state, selectedPath: action.value }; + case 'selectObjectToAddProperty': + return { ...state, selectedObjectToAddProperty: action.value }; + case 'saveProperty': + updatedProperties = set({ ...state.properties }, action.path, action.value); + return { + ...state, + selectedPath: null, + selectedObjectToAddProperty: null, + properties: updatedProperties, + }; + case 'deleteProperty': + updatedProperties = { ...state.properties }; + unset(updatedProperties, action.path); + return { + ...state, + properties: updatedProperties, + }; + case 'updatePropertyPath': + const property = get(state.properties, action.oldPath); + // Delete the property at the old path + unset(state.properties, action.oldPath); + // Add it to the new path + updatedProperties = set({ ...state.properties }, action.newPath, property); + return { + ...state, + properties: updatedProperties, + }; + + default: + throw new Error(`Unhandled action type: ${action!.type}`); + } +} + +interface Props { + children: React.ReactNode; + defaultProperties?: Record; +} + +export const PropertiesProvider = ({ children, defaultProperties = {} }: Props) => { + const [state, dispatch] = useReducer(propertiesReducer, { + properties: defaultProperties, + selectedPath: null, + selectedObjectToAddProperty: null, + }); + + return ( + + + {children} + + + ); +}; + +export const usePropertiesState = () => { + const context = useContext(PropertiesStateContext); + if (context === undefined) { + throw new Error('usePropertiesState must be used within a '); + } + return context; +}; + +export const usePropertiesDispatch = () => { + const context = useContext(PropertiesDispatchContext); + if (context === undefined) { + throw new Error('usePropertiesState must be used within a '); + } + return context; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx deleted file mode 100644 index d17049ddee4c5..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_manager.tsx +++ /dev/null @@ -1,104 +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 React, { Fragment } from 'react'; -import { EuiButton, EuiAccordion, EuiSpacer } from '@elastic/eui'; -import uuid from 'uuid'; - -import { - UseArray, - Form, - ArrayItem, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; - -import { PropertyEditor } from './property_editor'; - -interface Props { - form: Form; - parentType?: string; - path?: string; - fieldName?: string; -} - -export const PropertiesManager = ({ form, parentType = 'root', path, fieldName = '' }: Props) => { - const getArrayPath = () => { - if (parentType === 'root') { - return 'properties'; - } - - return parentType === 'text' || parentType === 'keyword' - ? `${path}fields` - : `${path}properties`; - }; - - const getAccordionButtonContent = () => { - return parentType === 'text' || parentType === 'keyword' - ? `${fieldName} child fields` - : `${fieldName} properties`; - }; - - const renderPropertiesTree = ({ - items, - addItem, - removeItem, - }: { - items: ArrayItem[]; - addItem: () => void; - removeItem: (id: number) => void; - }) => ( - - {items.length > 0 && ( - -
            - {items.map(({ id, path: itemPath, isNew }) => { - return ( -
          • - removeItem(id)} - isEditMode={!isNew} - /> -
          • - ); - })} -
          - -
          - )} - - - {parentType === 'text' || parentType === 'keyword' ? 'Add child field' : 'Add property'} - -
          - ); - - return ( - - {({ items, addItem, removeItem }) => { - if (parentType === 'root' || items.length === 0) { - // At the root level or if there aren't any property - // we don't add the accordion - return renderPropertiesTree({ items, addItem, removeItem }); - } - return ( - - {renderPropertiesTree({ items, addItem, removeItem })} - - ); - }} - - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx new file mode 100644 index 0000000000000..2ec7fec689315 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx @@ -0,0 +1,92 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { EuiConfirmModal, EuiOverlayMask, EuiTitle } from '@elastic/eui'; + +import { usePropertiesDispatch } from '../properties_contex'; +import { getNestedFieldMeta } from '../../helpers'; + +type DeletePropertyFunc = (property: Record, path: string) => void; + +interface Props { + children: (deleteProperty: DeletePropertyFunc) => React.ReactNode; +} + +interface State { + isModalOpen: boolean; + path: string | null; + property: Record | null; +} + +export const DeletePropertyProvider = ({ children }: Props) => { + const [state, setState] = useState({ isModalOpen: false, path: null, property: null }); + const dispatch = usePropertiesDispatch(); + + const closeModal = () => { + setState({ isModalOpen: false, path: null, property: null }); + }; + + const deleteProperty: DeletePropertyFunc = (property, path) => { + const { hasChildProperties } = getNestedFieldMeta(property); + + if (hasChildProperties) { + setState({ isModalOpen: true, property, path }); + } else { + dispatch({ type: 'deleteProperty', path }); + } + }; + + const confirmDelete = () => { + dispatch({ type: 'deleteProperty', path: state.path! }); + closeModal(); + }; + + const renderModal = () => { + const { property } = state; + const { nestedFieldPropName } = getNestedFieldMeta(property!); + const title = `Remove property '${property!.name}'?`; + const childrenCount = Object.keys(property![nestedFieldPropName!]).length; + + return ( + + + +

          + By deleting this property you will also delete its child{' '} + {childrenCount > 1 ? 'properties' : 'property'}, and all{' '} + {childrenCount > 1 ? 'their' : 'its'} possible nested properties. +

          + +

          Child properties that will also be deleted

          +
          +
            + {Object.keys(property![nestedFieldPropName!]) + .sort() + .map(name => ( +
          • {name}
          • + ))} +
          +
          +
          +
          + ); + }; + + return ( + + {children(deleteProperty)} + {state.isModalOpen && renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts new file mode 100644 index 0000000000000..d5941c1bef683 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './property_list_item'; +export * from './property_editor'; +export * from './save_property_provider'; +export * from './delete_property_provider'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx similarity index 53% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx index 81c75010fe392..1791ec6ecac3f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx @@ -8,66 +8,122 @@ import { EuiSpacer, EuiFlexGroup, EuiFlexItem, + EuiButton, EuiButtonEmpty, - EuiButtonIcon, + EuiForm, EuiFormRow, EuiSelect, } from '@elastic/eui'; + import { + useForm, UseField, - Form, FormDataProvider, FieldConfig, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; + ValidationConfig, +} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +import { nameConflictError } from '../../errors'; import { parametersDefinition, dataTypesDefinition, + getTypeFromSubType, ParameterName, DataType, SubType, -} from '../config'; -import { hasNestedProperties } from '../helpers'; -import { PropertyBasicParameters } from './property_basic_parameters'; -import { PropertiesManager } from './properties_manager'; -import { getComponentForParameter } from './parameters'; -import { getAdvancedSettingsCompForType } from './advanced_settings'; +} from '../../config'; +import { PropertyBasicParameters } from '../property_basic_parameters'; +import { getComponentForParameter } from '../parameters'; +import { getAdvancedSettingsCompForType } from '../advanced_settings'; interface Props { - form: Form; - onRemove?: () => void; - fieldPathPrefix?: string; - isDeletable?: boolean; - isEditMode?: boolean; + onSubmit: (property: Record) => void; + onCancel: () => void; + defaultValue?: Record; + parentObject: Record; + [key: string]: any; } +// We need to dynamically add the "nameValidation" as it validates +// that the field name value provided does not already exist on the parent object. +const updateNameParameterValidations = ( + fieldConfig: FieldConfig, + parentObject: Record, + initialValue = '' +): ValidationConfig[] => { + const nameValidation: ValidationConfig['validator'] = ({ path, value, form, formData }) => { + if (Object.keys(parentObject).some(key => key !== initialValue && key === value)) { + return nameConflictError(); + } + }; + return [...fieldConfig.validations!, { validator: nameValidation }]; +}; + const fieldConfig = (param: ParameterName): FieldConfig => parametersDefinition[param].fieldConfig || {}; const defaultValueParam = (param: ParameterName): unknown => typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; +const sanitizePropParameters = (parameters: Record): Record => + Object.entries(parameters).reduce( + (acc, [param, value]) => { + // IF a prop value is "index_default", we remove it + if (value !== 'index_default') { + acc[param] = value; + } + return acc; + }, + {} as any + ); + +const serializer = (property: Record) => { + // If a subType is present, use it as type for ES + if ({}.hasOwnProperty.call(property, 'subType')) { + property.type = property.subType; + delete property.subType; + } + return sanitizePropParameters(property); +}; + +const deSerializer = (property: Record) => { + if (!(dataTypesDefinition as any)[property.type]) { + const type = getTypeFromSubType(property.type); + if (!type) { + throw new Error( + `Property type "${property.type}" not recognized and no subType was found for it.` + ); + } + property.subType = property.type; + property.type = type; + } + + return property; +}; + export const PropertyEditor = ({ - form, - fieldPathPrefix = '', - onRemove = () => undefined, - isDeletable = true, - isEditMode = false, + onSubmit, + onCancel, + defaultValue, + parentObject, + ...rest }: Props) => { const [isAdvancedSettingsVisible, setIsAdvancedSettingsVisible] = useState(false); - const renderNestedProperties = (selectedType: DataType, fieldName: string) => - hasNestedProperties(selectedType) ? ( - - - - - ) : null; + + const { form } = useForm({ defaultValue, serializer, deSerializer }); + const isEditMode = typeof defaultValue !== 'undefined'; + + const submitForm = async () => { + const { isValid, data: formData } = await form.onSubmit(); + if (isValid) { + const data = + defaultValue && defaultValue.properties + ? { ...formData, properties: defaultValue.properties } + : formData; + onSubmit(data); + } + }; const toggleAdvancedSettings = () => { setIsAdvancedSettingsVisible(previous => !previous); @@ -83,40 +139,44 @@ export const PropertyEditor = ({
          - +
          ); }; return ( - + {formData => { - const selectedDatatype = formData[`${fieldPathPrefix}type`] as DataType; + const selectedDatatype = formData.type as DataType; const typeDefinition = dataTypesDefinition[selectedDatatype]; return ( -
          - + + {/* Field name */} {/* Field type */} )} - {/* Empty flex item to fill the space in between */} - - - {/* Delete field button */} - {isDeletable && ( - - - - )} {((typeDefinition && typeDefinition.basicParameters) || @@ -193,7 +243,6 @@ export const PropertyEditor = ({ @@ -211,13 +260,26 @@ export const PropertyEditor = ({ {renderAdvancedSettings(selectedDatatype)} - - {_formData => { - const nameValue = _formData[`${fieldPathPrefix}name`] as string; - return renderNestedProperties(selectedDatatype, nameValue); - }} - -
          + + + + + Cancel + + + + + {isEditMode ? 'Done' : 'Add'} + + + + ); }}
          diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx new file mode 100644 index 0000000000000..645e5eee44312 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx @@ -0,0 +1,146 @@ +/* + * 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, { Fragment, useState } from 'react'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; + +import { PropertyView } from './property_view'; +import { PropertyEditor } from './property_editor'; +import { SavePropertyProvider } from './save_property_provider'; +import { DeletePropertyProvider } from './delete_property_provider'; +import { Tree, TreeItem } from '../tree'; +import { usePropertiesState, usePropertiesDispatch } from '../properties_contex'; +import { getNestedFieldMeta, getParentObject } from '../../helpers'; +interface Props { + name: string; + path: string; + property: Record; + nestedDepth: number; +} + +export const PropertyListItem = ({ name, property, path, nestedDepth }: Props) => { + const { selectedPath, selectedObjectToAddProperty, properties } = usePropertiesState(); + const dispatch = usePropertiesDispatch(); + + const { + hasChildProperties, + nestedFieldPropName, + allowChildProperty, + childProperties, + } = getNestedFieldMeta(property); + + const isEditMode = selectedPath === path; + const isCreateMode = selectedObjectToAddProperty === path; + const isPropertyEditorVisible = isEditMode || isCreateMode; + const parentObject = getParentObject(path, properties); + const [showChildren, setShowChildren] = useState(isPropertyEditorVisible); + + const renderActionButtons = () => ( + + {allowChildProperty && ( + + dispatch({ type: 'selectObjectToAddProperty', value: path })} + iconType="plusInCircle" + aria-label="Add property" + disabled={selectedPath !== null || selectedObjectToAddProperty !== null} + /> + + )} + + dispatch({ type: 'selectPath', value: path })} + iconType="pencil" + aria-label="Edit property" + disabled={selectedPath !== null || selectedObjectToAddProperty !== null} + /> + + + + {deleteProperty => ( + deleteProperty({ name, ...property }, path)} + iconType="trash" + aria-label="Delete property" + disabled={selectedPath !== null || selectedObjectToAddProperty !== null} + /> + )} + + + + ); + + const renderEditForm = (style = {}) => ( + + {saveProperty => ( + ) => { + // Make sure the object is unfolded + setShowChildren(true); + saveProperty({ newProperty, oldProperty: property, path, isEditMode, isCreateMode }); + }} + onCancel={() => + isCreateMode + ? dispatch({ type: 'selectObjectToAddProperty', value: null }) + : dispatch({ type: 'selectPath', value: null }) + } + defaultValue={isCreateMode ? undefined : { name, ...property }} + parentObject={isCreateMode ? property[nestedFieldPropName!] : parentObject} + style={{ ...style, marginLeft: `${nestedDepth * -24 + 1}px` }} + /> + )} + + ); + + const renderPropertiesTree = () => ( + } + rightHeaderContent={renderActionButtons()} + isOpen={isPropertyEditorVisible ? true : showChildren} + onToggle={() => setShowChildren(prev => !prev)} + > + + {isPropertyEditorVisible && renderEditForm({ marginTop: 0, marginBottom: '12px' })} + {Object.entries(childProperties) + // Make sure to display the fields in alphabetical order + .sort(([a], [b]) => (a < b ? -1 : 1)) + .map(([childName, childProperty], i) => ( + + + + ))} + + + ); + + const renderNoChildren = () => ( + + + + + + {renderActionButtons()} + + {isPropertyEditorVisible && renderEditForm()} + + ); + + return allowChildProperty ? ( + + {isPropertyEditorVisible &&
          } + {hasChildProperties ? renderPropertiesTree() : renderNoChildren()} +
          + ) : ( + renderNoChildren() + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx new file mode 100644 index 0000000000000..22102ce260263 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx @@ -0,0 +1,50 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiText, EuiTextColor, EuiFlexItem, EuiFlexGroup, EuiBadge } from '@elastic/eui'; + +interface Props { + name: string; + property: Record; +} + +export const PropertyView = ({ name, property }: Props) => { + const badges = ( + + {['index', 'doc_values', 'store', 'fielddata'] + .filter(field => Boolean(property[field])) + .map((field, i) => ( + + {field} + + ))} + + ); + + return ( + + {/* Name & Type */} + + + + {name} + + + + ({property.type}) + + + + + + {/* Badges */} + + {badges} + + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx new file mode 100644 index 0000000000000..c2398530ce914 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx @@ -0,0 +1,176 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { EuiConfirmModal, EuiOverlayMask, EuiTitle } from '@elastic/eui'; + +import { usePropertiesDispatch } from '../properties_contex'; +import { getNestedFieldMeta } from '../../helpers'; + +type SavePropertyFunc = (args: { + newProperty: Record; + oldProperty?: Record; + path: string; + isEditMode: boolean; + isCreateMode: boolean; +}) => void; + +interface Props { + children: (saveProperty: SavePropertyFunc) => React.ReactNode; +} + +interface State { + isModalOpen: boolean; + path: string | null; + newProperty: Record | null; + oldProperty: Record | null; +} + +export const SavePropertyProvider = ({ children }: Props) => { + const [state, setState] = useState({ + isModalOpen: false, + path: null, + newProperty: null, + oldProperty: null, + }); + const dispatch = usePropertiesDispatch(); + + const closeModal = () => { + setState({ isModalOpen: false, path: null, newProperty: null, oldProperty: null }); + }; + + const saveProperty: SavePropertyFunc = ({ + newProperty, + oldProperty, + path, + isCreateMode, + isEditMode, + }) => { + const { name: updatedName, ...rest } = newProperty; + + const handleUpdateFieldName = (newName: string): string => { + // The name has been updated, we need to + // 1. Change the property path to the new path + // 2. Replace the old property at the new path + const pathToArray = path.split('.'); + pathToArray[pathToArray.length - 1] = newName; + const newPath = pathToArray.join('.'); + + dispatch({ type: 'updatePropertyPath', oldPath: path, newPath }); + return newPath; + }; + + const handleUpdateFieldType = ( + oldType: string, + newType: string + ): { requiresConfirmation: boolean } => { + const { hasChildProperties } = getNestedFieldMeta(oldProperty!); + + if (!hasChildProperties) { + // No child properties will be deleted, no confirmation needed. + return { requiresConfirmation: false }; + } + + let requiresConfirmation = false; + + if (oldType === 'text' && newType !== 'keyword') { + requiresConfirmation = true; + } else if (oldType === 'keyword' && newType !== 'text') { + requiresConfirmation = true; + } else if (oldType === 'object' && newType !== 'nested') { + requiresConfirmation = true; + } else if (oldType === 'nested' && newType !== 'object') { + requiresConfirmation = true; + } + + return { requiresConfirmation }; + }; + + let pathToSaveProperty = path; + + if (isEditMode) { + if (updatedName !== name) { + pathToSaveProperty = handleUpdateFieldName(updatedName); + } + if (rest.type !== oldProperty!.type) { + // We need to check if, by changing the type, we need + // to delete the possible child properties ("fields" or "properties") + // and warn the user about it. + const { requiresConfirmation } = handleUpdateFieldType(oldProperty!.type, rest.type); + if (requiresConfirmation) { + setState({ isModalOpen: true, newProperty, oldProperty: oldProperty!, path }); + return; + } + } + } else if (isCreateMode) { + if (oldProperty) { + // If there is an "oldProperty" it means we want to add the property + // in either its "properties" or "fields" + // nestedFieldPropName is "properties" (for object and nested types) + // or "fields" (for text and keyword types). + const { nestedFieldPropName } = getNestedFieldMeta(oldProperty!); + pathToSaveProperty = `${path}.${nestedFieldPropName}.${updatedName}`; + } else { + // If there are no "oldProperty" we add the property to the top level + // "properties" object. + pathToSaveProperty = updatedName; + } + } + dispatch({ type: 'saveProperty', path: pathToSaveProperty, value: rest }); + }; + + const confirmTypeUpdate = () => { + delete state.newProperty!.fields; + delete state.newProperty!.properties; + dispatch({ type: 'saveProperty', path: state.path!, value: state.newProperty! }); + closeModal(); + }; + + const renderModal = () => { + const { newProperty, oldProperty } = state; + const title = `Confirm change '${newProperty!.name}' type to "${newProperty!.type}".`; + const { nestedFieldPropName } = getNestedFieldMeta(oldProperty!); + const childrenCount = Object.keys(oldProperty![nestedFieldPropName!]).length; + + return ( + + + +

          + By changing the type of this property you will also delete its child{' '} + {childrenCount > 1 ? 'properties' : 'property'}, and all{' '} + {childrenCount > 1 ? 'their' : 'its'} possible nested properties. +

          + +

          Nested {childrenCount > 1 ? 'properties' : 'property'} that will be removed

          +
          +
            + {Object.keys(oldProperty![nestedFieldPropName!]) + .sort() + .map(name => ( +
          • {name}
          • + ))} +
          +
          +
          +
          + ); + }; + + return ( + + {children(saveProperty)} + {state.isModalOpen && renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts new file mode 100644 index 0000000000000..c85cd21b1c5d0 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './tree'; +export * from './tree_item'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx new file mode 100644 index 0000000000000..f5b4ea2e1b033 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx @@ -0,0 +1,67 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiIcon } from '@elastic/eui'; + +interface Props { + children: React.ReactNode; + defaultIsOpen?: boolean; + isOpen?: boolean; + onToggle?: () => void; + headerContent?: React.ReactNode; + rightHeaderContent?: React.ReactNode; +} + +export const Tree = ({ + children, + headerContent, + rightHeaderContent, + defaultIsOpen = false, + isOpen, + onToggle = () => undefined, +}: Props) => { + const hasHeader = Boolean(headerContent); + const isControlled = typeof isOpen !== 'undefined'; + const [showChildren, setShowChildren] = useState(defaultIsOpen); + const toggleShowChildren = () => setShowChildren(previous => !previous); + + const getIsOpen = () => (isControlled ? isOpen : showChildren); + const onMainBtnClick = () => (isControlled ? onToggle() : toggleShowChildren()); + + return ( + + {hasHeader && ( + + + + + {rightHeaderContent && {rightHeaderContent}} + + )} + {getIsOpen() && ( + + {hasHeader && } +
            {children}
          +
          + )} +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx new file mode 100644 index 0000000000000..72c78767229e7 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx @@ -0,0 +1,15 @@ +/* + * 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'; + +interface Props { + children: React.ReactNode; +} + +export const TreeItem = ({ children }: Props) => { + return
        • {children}
        • ; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index f135447165fe4..58fd857b020b4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -16,9 +16,6 @@ import { import { toInt } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib'; -import { nameConflictError } from '../errors'; -import { ERROR_CODES } from '../constants'; - export type ParameterName = | 'name' | 'type' @@ -72,74 +69,6 @@ export const parametersDefinition: { message: 'Spaces are not allowed in the name.', }), }, - { - validator: ({ path, value, form, formData }) => { - const regEx = /(.+)(\d+\.name)$/; - const regExResult = regEx.exec(path); - - if (regExResult) { - const { 1: parentPath } = regExResult; - // Get all the "name" properties on the parent path - const namePropertyPaths = Object.keys(formData).filter(key => { - // Make sure we are filtering *only* the properties at the - // same nested object level - const isSameNestedLevel = Math.abs(key.length - path.length) <= 3; - - return ( - key !== path && - isSameNestedLevel && - key.startsWith(parentPath) && - key.endsWith('name') - ); - }); - - // Keep a referende of all the field name that have - // a conflict with the current field. - const conflictPaths: string[] = []; - - for (const namePath of namePropertyPaths) { - const nameField = form.getFields()[namePath]; - const nameFieldConflictError = nameField.errors.filter( - err => err.code === ERROR_CODES.NAME_CONFLICT - )[0]; - - let error; - if (formData[namePath] === value) { - conflictPaths.push(namePath); - if (!nameFieldConflictError) { - error = nameConflictError([path]); - } else { - error = nameConflictError([...nameFieldConflictError.conflictPaths, path]); - } - } else if ( - nameFieldConflictError && - (nameFieldConflictError.conflictPaths as string[]).some( - conflictPath => conflictPath === path - ) - ) { - if ((nameFieldConflictError.conflictPaths as string[]).length > 1) { - const updatedConflictPaths = (nameFieldConflictError.conflictPaths as string[]).filter( - p => p !== path - ); - error = nameConflictError(updatedConflictPaths); - } else { - nameField.setErrors([]); - } - } - - if (error) { - // Update the validation on the other field - nameField.setErrors([error]); - } - } - - if (conflictPaths.length) { - // Update the validation on the current field being validated - return nameConflictError(conflictPaths); - } - } - }, - }, ], }, }, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts index 02c551bcbf5f9..d3a8e5bad1776 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts @@ -12,8 +12,7 @@ import { ERROR_CODES } from './constants'; * Error creators */ -export const nameConflictError = (conflictPaths: string[]): ValidationError => ({ +export const nameConflictError = (): ValidationError => ({ code: ERROR_CODES.NAME_CONFLICT, message: 'A field with the same name already exists.', - conflictPaths, }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts index 57d9d13201529..afa429676f04f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts @@ -3,6 +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 { get } from 'lodash'; import { DataType } from './config'; export const hasNestedProperties = (selectedDatatype: DataType) => @@ -11,44 +12,63 @@ export const hasNestedProperties = (selectedDatatype: DataType) => selectedDatatype === 'text' || selectedDatatype === 'keyword'; -export const propertiesArrayToObject = (properties?: any[]): any => - properties - ? properties.reduce((acc, property) => { - const { name, ...rest } = property; - acc[property.name] = rest; - if (hasNestedProperties(rest.type)) { - // Recursively convert Array to Object - if (rest.properties) { - rest.properties = propertiesArrayToObject(rest.properties); - } else if (rest.fields) { - rest.fields = propertiesArrayToObject(rest.fields); - } - } - return acc; - }, {}) - : properties; - -export const propertiesObjectToArray = ( - properties: { - [key: string]: Record; - } = {} -): any[] => - Object.entries(properties).map(([name, property]) => { - if (hasNestedProperties(property.type)) { - // Recursively convert Object to Array - if (property.properties) { - return { - name, - ...property, - properties: propertiesObjectToArray(property.properties), - }; - } else if (property.fields) { - return { - name, - ...property, - fields: propertiesObjectToArray(property.fields), - }; - } +const getNestedFieldsPropName = (selectedDatatype: DataType) => { + if (selectedDatatype === 'text' || selectedDatatype === 'keyword') { + return 'fields'; + } else if (selectedDatatype === 'object' || selectedDatatype === 'nested') { + return 'properties'; + } + return undefined; +}; + +export const getNestedFieldMeta = ( + property: Record +): { + hasChildProperties: boolean; + allowChildProperty: boolean; + nestedFieldPropName: 'fields' | 'properties' | undefined; + childProperties: Record; +} => { + const nestedFieldPropName = getNestedFieldsPropName(property.type); + const hasChildProperties = + typeof nestedFieldPropName !== 'undefined' && + Boolean(property[nestedFieldPropName]) && + Object.keys(property[nestedFieldPropName]).length > 0; + + const allowChildProperty = Boolean(nestedFieldPropName); + const childProperties = allowChildProperty && property[nestedFieldPropName!]; + return { hasChildProperties, nestedFieldPropName, allowChildProperty, childProperties }; +}; + +export const getParentObject = (path: string, object = {}): Record => { + const pathToArray = path.split('.'); + if (pathToArray.length === 1) { + return object; + } + const parentPath = pathToArray.slice(0, -1).join('.'); + return get(object, parentPath); +}; + +// We use an old version of lodash that does not have the _.unset() utility method. +// We implement our own here. +export const unset = (object: Record, path: string): boolean => { + const pathToArray = path.split('.'); + let hasBeenRemoved: boolean; + + if (pathToArray.length === 1) { + const [prop] = pathToArray; + hasBeenRemoved = {}.hasOwnProperty.call(object, prop); + delete object[prop]; + } else { + const parentObject = getParentObject(path, object); + if (!parentObject || typeof parentObject !== 'object') { + hasBeenRemoved = false; + } else { + const prop = pathToArray[pathToArray.length - 1]; + hasBeenRemoved = {}.hasOwnProperty.call(parentObject, prop); + delete (parentObject as any)[prop]; } - return { name, ...property }; - }); + } + + return hasBeenRemoved; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index f4408d4d2c3d4..6ab4d2790eec5 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,120 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; -import { EuiTitle, EuiSpacer, EuiForm } from '@elastic/eui'; -import { - useForm, - UseField, -} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { - FormRow, - Field, -} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +import React, { useState, useEffect, useRef } from 'react'; -import { schema } from './form.schema'; -import { PropertiesManager } from './components'; -import { propertiesArrayToObject, propertiesObjectToArray } from './helpers'; -import { dataTypesDefinition, getTypeFromSubType } from './config'; -import { DYNAMIC_SETTING_OPTIONS } from './constants'; +import { + ConfigurationForm, + PropertiesProvider, + DocumentFields, + DocumentFieldsState, +} from './components'; interface Props { - setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: Mappings }>) => void; - defaultValue?: Mappings; - areErrorsVisible?: boolean; + setGetDataHandler: ( + handler: () => Promise<{ isValid: boolean; data: Record }> + ) => void; + onStateUpdate: (state: State) => void; + defaultValue?: Record; } -export interface Mappings { - [key: string]: any; +export interface State { + isValid: boolean; + isEditingProperty: boolean; + properties: Record; } -const sanitizePropParameters = (parameters: Record): Record => - Object.entries(parameters).reduce( - (acc, [param, value]) => { - // IF a prop value is "index_default", we remove it - if (value !== 'index_default') { - acc[param] = value; - } - return acc; - }, - {} as any - ); - -const serializeProperties = (properties: any[]) => - properties.map(prop => { - // If a subType is present, use it as type for ES - if ({}.hasOwnProperty.call(prop, 'subType')) { - prop.type = prop.subType; - delete prop.subType; - } +export type Mappings = Record; - return sanitizePropParameters(prop); - }); +type GetFormDataHandler = () => Promise<{ isValid: boolean; data: Record }>; -const deSerializeProperties = (properties: { [key: string]: any }) => { - Object.entries(properties).forEach(([name, prop]: [string, any]) => { - // Check if the type provided is a subType (e.g: "float" is a subType of the "numeric" type in the UI) - if (!(dataTypesDefinition as any)[prop.type]) { - const type = getTypeFromSubType(prop.type); - if (!type) { - throw new Error( - `Property type "${prop.type}" not recognized and no subType was found for it.` - ); - } - prop.subType = prop.type; - prop.type = type; - } +export const MappingsEditor = ({ setGetDataHandler, onStateUpdate, defaultValue = {} }: Props) => { + const [state, setState] = useState({ + isValid: true, + isEditingProperty: false, + properties: {}, }); - return properties; -}; + const getConfigurationFormData = useRef(() => + Promise.resolve({ + isValid: true, + data: {}, + }) + ); -const serializer = (data: Record): Record => ({ - ...data, - properties: propertiesArrayToObject(serializeProperties(data.properties as any[])), -}); + useEffect(() => { + setGetDataHandler(async () => { + const { isValid, data } = await getConfigurationFormData.current(); + return { + isValid, + data: { ...data, properties: state.properties }, + }; + }); + }, []); -const deSerializer = (data: Record): Record => ({ - ...data, - properties: propertiesObjectToArray( - deSerializeProperties(data.properties as { [key: string]: any }) - ), -}); + useEffect(() => { + onStateUpdate(state); + }, [state]); -export const MappingsEditor = ({ - setGetDataHandler, - areErrorsVisible = true, - defaultValue, -}: Props) => { - const { form } = useForm({ schema, serializer, deSerializer, defaultValue }); + const setGetConfigurationFormDataHandler = (handler: GetFormDataHandler) => + (getConfigurationFormData.current = handler); - useEffect(() => { - setGetDataHandler(form.onSubmit); - }, [form]); + const onDocumentFieldsUpdate = (docFieldsState: DocumentFieldsState) => { + setState(prev => ({ + ...prev, + isEditingProperty: docFieldsState.isEditing, + properties: docFieldsState.properties, + })); + }; return ( - +
          {/* Global Mappings configuration */} - - - - - - + setState(prev => ({ ...prev, isValid }))} + defaultValue={defaultValue} + /> {/* Document fields */} - -

          Document fields

          -
          - - - + + + +
          ); }; From 6afa76f7c6e7a5ad5de6fcf362a420b78fc2b470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 9 Aug 2019 16:36:10 +0200 Subject: [PATCH 30/64] Update styles.scss --- .../static/ui/components/mappings_editor/_styles.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss index f13d430809652..6c9611d8da702 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss @@ -10,12 +10,6 @@ position: relative; padding-bottom: $gutter; z-index: 1; - - .action-buttons { - position: absolute; - top: -38px; - right: 0; - } } .property-list-item__overlay { From af93feac2d49a1aa648efde339bad43e810c8fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 9 Aug 2019 16:37:53 +0200 Subject: [PATCH 31/64] Add Hook form lib (To be reverted) --- .../static/forms/components/field.tsx | 210 ++++++++++ .../static/forms/components/form_row.tsx | 54 +++ .../static/forms/components/index.ts | 21 + .../static/forms/errors/constants.ts | 24 ++ .../static/forms/errors/errors.ts | 55 +++ .../static/forms/errors/index.ts | 21 + .../components/form_data_provider.ts | 65 +++ .../forms/hook_form_lib/components/index.ts | 22 + .../hook_form_lib/components/use_array.ts | 87 ++++ .../hook_form_lib/components/use_field.tsx | 88 ++++ .../static/forms/hook_form_lib/constants.ts | 36 ++ .../static/forms/hook_form_lib/hooks/index.ts | 21 + .../forms/hook_form_lib/hooks/use_field.ts | 385 ++++++++++++++++++ .../forms/hook_form_lib/hooks/use_form.ts | 192 +++++++++ .../static/forms/hook_form_lib/index.ts | 27 ++ .../static/forms/hook_form_lib/lib/index.ts | 21 + .../static/forms/hook_form_lib/lib/subject.ts | 51 +++ .../static/forms/hook_form_lib/lib/utils.ts | 116 ++++++ .../static/forms/hook_form_lib/types.ts | 149 +++++++ .../static/forms/lib/de_serializers.ts | 36 ++ .../static/forms/lib/field_formatters.ts | 28 ++ .../lib/field_validators/contains_char.ts | 41 ++ .../forms/lib/field_validators/empty_field.ts | 37 ++ .../forms/lib/field_validators/index.ts | 25 ++ .../forms/lib/field_validators/index_name.ts | 47 +++ .../forms/lib/field_validators/min_length.ts | 34 ++ .../min_selectable_selection.ts | 40 ++ .../static/forms/lib/field_validators/url.ts | 38 ++ .../static/forms/lib/index.ts | 22 + .../static/forms/lib/serializers.ts | 49 +++ .../static/validators/array/has_max_length.ts | 20 + .../static/validators/array/has_min_length.ts | 20 + .../static/validators/array/index.ts | 22 + .../static/validators/array/is_empty.ts | 20 + .../validators/string/contains_chars.ts | 32 ++ .../static/validators/string/ends_with.ts | 20 + .../validators/string/has_max_length.ts | 20 + .../validators/string/has_min_length.ts | 20 + .../static/validators/string/index.ts | 26 ++ .../static/validators/string/is_empty.ts | 20 + .../static/validators/string/is_url.ts | 47 +++ .../static/validators/string/starts_with.ts | 20 + .../plugins/index_management/public/app.js | 2 + .../sections/create_index/create_index.tsx | 152 +++++++ .../public/sections/create_index/index.ts | 7 + 45 files changed, 2490 insertions(+) create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts create mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts create mode 100644 x-pack/legacy/plugins/index_management/public/sections/create_index/create_index.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx new file mode 100644 index 0000000000000..01ee046757dcc --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx @@ -0,0 +1,210 @@ +/* + * 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, + EuiFieldNumber, + EuiSelect, + EuiComboBox, + EuiSwitch, + EuiCheckbox, + EuiSelectable, + EuiPanel, + EuiComboBoxOptionProps, +} from '@elastic/eui'; +import uuid from 'uuid'; + +import { + Field as FieldType, + FIELD_TYPES, + VALIDATION_TYPES, + FieldValidateResponse, +} from '../hook_form_lib'; + +interface Props { + field: FieldType; + fieldProps?: Record; +} + +export const Field = ({ field, fieldProps = {} }: Props) => { + let isInvalid: boolean; + let errorMessage: string | null; + + if (field.type === FIELD_TYPES.COMBO_BOX) { + // Errors for the comboBox value (the "array") + const errorMessageField = field.form.isSubmitted ? field.getErrorsMessages() : null; + + // Errors for comboBox option added (the array "item") + const errorMessageArrayItem = field.getErrorsMessages({ + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }); + + isInvalid = field.errors.length + ? field.form.isSubmitted || errorMessageArrayItem !== null + : false; + + // Concatenate error messages. + errorMessage = + errorMessageField && errorMessageArrayItem + ? `${errorMessageField}, ${errorMessageArrayItem}` + : errorMessageField + ? errorMessageField + : errorMessageArrayItem; + } else { + isInvalid = !field.isUpdating && (field.form.isSubmitted && field.errors.length > 0); + errorMessage = + !field.isUpdating && field.errors.length ? (field.errors[0].message as string) : null; + } + + const onCreateComboOption = async (value: string) => { + // Note: for now, we assume that all validations for a comboBox array item are synchronous + // If there is a need to support asynchronous validation, we'll work on it. + const { isValid } = field.validate({ + value, + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }) as FieldValidateResponse; + + if (!isValid) { + field.setValue(field.value as string[]); + return; + } + + 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); + } + }; + + const doDisplayLabelOnTop = + field.type !== FIELD_TYPES.TOGGLE && field.type !== FIELD_TYPES.CHECKBOX; + + const renderField = () => { + switch (field.type) { + case FIELD_TYPES.NUMBER: + return ( + + ); + case FIELD_TYPES.SELECT: + return ( + { + field.setValue(e.target.value); + }} + hasNoInitialSelection={true} + isInvalid={false} + {...(fieldProps as { options: any; [key: string]: any })} + /> + ); + case FIELD_TYPES.COMBO_BOX: + return ( + ({ label: v }))} + onCreateOption={onCreateComboOption} + onChange={onComboChange} + onSearchChange={onSearchComboChange} + fullWidth + {...fieldProps} + /> + ); + case FIELD_TYPES.TOGGLE: + return ( + + ); + case FIELD_TYPES.CHECKBOX: + return ( + + ); + case FIELD_TYPES.MULTI_SELECT: + return ( + { + field.setValue(options); + }} + options={field.value as any[]} + {...fieldProps} + > + {(list, search) => ( + + {search} + {list} + + )} + + ); + default: + return ( + + ); + } + }; + + return ( + + {renderField()} + + ); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx new file mode 100644 index 0000000000000..7eb3e465e315a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx @@ -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 { EuiDescribedFormGroup, EuiTitle } from '@elastic/eui'; +import { Field as FieldType } from '../hook_form_lib'; +import { Field } from './field'; + +interface Props { + title: string | JSX.Element; + description?: string | JSX.Element; + field?: FieldType; + fieldProps?: Record; + children?: React.ReactNode; +} + +export const FormRow = ({ title, description, field, fieldProps = {}, children }: Props) => { + // If a string is provided, create a default Euititle of size "m" + const _title = + typeof title === 'string' ? ( + +

          {title}

          +
          + ) : ( + title + ); + + if (!children && !field) { + throw new Error('You need to provide either children or a field to the FormRow'); + } + + return ( + + {children ? children : } + + ); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts new file mode 100644 index 0000000000000..de9e85ebf2e24 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts @@ -0,0 +1,21 @@ +/* + * 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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts new file mode 100644 index 0000000000000..87dd6c95c8840 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +// Error codes +export const ERR_FIELD_MISSING = 'ERR_FIELD_MISSING'; +export const ERR_MIN_LENGTH = 'ERR_MIN_LENGTH'; +export const ERR_MIN_SELECTION = 'ERR_MIN_SELECTION'; +export const ERR_FIELD_FORMAT = 'ERR_FIELD_FORMAT'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts new file mode 100644 index 0000000000000..16f56fbfd2d75 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts @@ -0,0 +1,55 @@ +/* + * 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 { + ERR_FIELD_FORMAT, + ERR_FIELD_MISSING, + ERR_MIN_LENGTH, + ERR_MIN_SELECTION, +} from './constants'; + +export const fieldMissingError = (fieldName: string, message = 'Field missing') => ({ + code: ERR_FIELD_MISSING, + fieldName, + message, +}); + +export const minLengthError = ( + length: number, + message = (error: any) => `Must have a minimun length of ${error.length}.` +) => ({ + code: ERR_MIN_LENGTH, + length, + message, +}); + +export const minSelectionError = ( + length: number, + message = (error: any) => `Must select at least ${error.length} items.` +) => ({ + code: ERR_MIN_SELECTION, + length, + message, +}); + +export const formatError = (format: string, message = 'Format error') => ({ + code: ERR_FIELD_FORMAT, + format, + message, +}); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts new file mode 100644 index 0000000000000..1fda395a4d7e5 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts @@ -0,0 +1,21 @@ +/* + * 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 './errors'; +export * from './constants'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts new file mode 100644 index 0000000000000..861ae32e01875 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -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 { useState, useEffect, useRef } from 'react'; + +import { Form, FormData } from '../types'; +import { Subscription } from '../lib'; + +interface Props { + children: (formData: FormData) => JSX.Element | null; + form: Form; + pathsToWatch?: string | string[]; +} + +export const FormDataProvider = ({ children, form, pathsToWatch }: Props) => { + const [formData, setFormData] = useState({}); + const previousState = useRef({}); + const subscription = useRef(null); + + useEffect(() => { + let didUnsubscribe = false; + subscription.current = form.__formData$.current.subscribe(data => { + if (didUnsubscribe) { + return; + } + // To avoid re-rendering the children for updates on the form data + // that we are **not** interested in, we can specify one or multiple path(s) + // to watch. + if (pathsToWatch) { + const valuesToWatchToArray = Array.isArray(pathsToWatch) + ? (pathsToWatch as string[]) + : ([pathsToWatch] as string[]); + if (valuesToWatchToArray.some(value => previousState.current[value] !== data[value])) { + previousState.current = data; + setFormData(data); + } + } else { + setFormData(data); + } + }); + + return () => { + didUnsubscribe = true; + subscription.current!.unsubscribe(); + }; + }, [pathsToWatch]); + + return children(formData); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts new file mode 100644 index 0000000000000..307b71f8d86b4 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts @@ -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 './use_field'; +export * from './use_array'; +export * from './form_data_provider'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts new file mode 100644 index 0000000000000..be9c90484e5d4 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -0,0 +1,87 @@ +/* + * 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 { useState, useRef } from 'react'; +import { Form } from '../types'; + +interface Props { + path: string; + form: Form; + initialNumberOfItems?: number; + children: (args: { + items: ArrayItem[]; + addItem: () => void; + removeItem: (id: number) => void; + }) => JSX.Element; +} + +export interface ArrayItem { + id: number; + path: string; + isNew: boolean; +} + +export const UseArray = ({ path, form, initialNumberOfItems, children }: Props) => { + const defaultValues = form.getFieldDefaultValue(path) as any[]; + const uniqueId = useRef(0); + + const getInitialRowsFromValues = (values: any[]): ArrayItem[] => + values.map((_, index) => ({ + id: uniqueId.current++, + path: `${path}.${index}`, + isNew: false, + })); + + const getNewItemAtIndex = (index: number): ArrayItem => ({ + id: uniqueId.current++, + path: `${path}.${index}`, + isNew: true, + }); + + const initialState = defaultValues + ? getInitialRowsFromValues(defaultValues) + : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); + + const [items, setItems] = useState(initialState); + + const updatePaths = (_rows: ArrayItem[]) => + _rows.map( + (row, index) => + ({ + ...row, + path: `${path}.${index}`, + } as ArrayItem) + ); + + const addItem = () => { + setItems(previousItems => { + const itemIndex = previousItems.length; + return [...previousItems, getNewItemAtIndex(itemIndex)]; + }); + }; + + const removeItem = (id: number) => { + setItems(previousItems => { + const updatedItems = previousItems.filter(item => item.id !== id); + return updatePaths(updatedItems); + }); + }; + + return children({ items, addItem, removeItem }); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx new file mode 100644 index 0000000000000..761d6c9fe9736 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -0,0 +1,88 @@ +/* + * 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, { useEffect } from 'react'; + +import { Form, Field as FieldType, FieldConfig } from '../types'; +import { useField } from '../hooks'; + +interface Props { + path: string; + config?: FieldConfig; + defaultValue?: unknown; + form: Form; + component?: (({ field }: { field: FieldType } & any) => JSX.Element) | 'input'; + componentProps?: any; + children?: (field: FieldType) => JSX.Element; +} + +export const UseField = ({ + path, + config, + form, + defaultValue = form.getFieldDefaultValue(path), + component = 'input', + componentProps = {}, + children, +}: Props) => { + if (!config) { + config = form.__readFieldConfigFromSchema(path); + } + + // Don't modify the config object + const configCopy = + typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config }; + + if (!configCopy.path) { + configCopy.path = path; + } else { + if (configCopy.path !== path) { + throw new Error( + `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".` + ); + } + } + + const field = useField(form, path, configCopy); + + // Remove field from form when it is unmounted or if its path changes + useEffect(() => { + return () => { + form.__removeField(path); + }; + }, [path]); + + // Children prevails over anything else provided. + if (children) { + return children!(field); + } + + if (component === 'input') { + return ( + + ); + } + + return component({ field, ...componentProps }); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts new file mode 100644 index 0000000000000..89ea91485a2d0 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +// Field types +export const FIELD_TYPES = { + TEXT: 'text', + NUMBER: 'number', + TOGGLE: 'toggle', + CHECKBOX: 'checkbox', + COMBO_BOX: 'comboBox', + SELECT: 'select', + MULTI_SELECT: 'multiSelect', +}; + +// Validation types +export const VALIDATION_TYPES = { + FIELD: 'field', // Default validation error (on the field value) + ASYNC: 'async', // Throw from asynchronous validations + ARRAY_ITEM: 'arrayItem', // If the field value is an Array, this error would be thrown if an _item_ of the array is invalid +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts new file mode 100644 index 0000000000000..6a04a592227f9 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { useField } from './use_field'; +export { useForm } from './use_form'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts new file mode 100644 index 0000000000000..b2bac5da64333 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -0,0 +1,385 @@ +/* + * 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 { useState, useEffect, useRef } from 'react'; + +import { + Form, + Field, + FieldConfig, + FieldValidateResponse, + ValidationConfig, + ValidationError, +} from '../types'; +import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; + +export const useField = (form: Form, path: string, config: FieldConfig = {}) => { + const { + type = FIELD_TYPES.TEXT, + defaultValue = '', + label = '', + helpText = '', + validations = [], + formatters = [], + fieldsToValidateOnChange = [path], + isValidationAsync = false, + errorDisplayDelay = form.options.errorDisplayDelay, + serializer = (value: unknown) => value, + deSerializer = (value: unknown) => value, + } = config; + + const [value, setStateValue] = useState( + typeof defaultValue === 'function' ? deSerializer(defaultValue()) : deSerializer(defaultValue) + ); + const [errors, setErrors] = useState([]); + const [isPristine, setPristine] = useState(true); + const [isValidating, setValidating] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const validateCounter = useRef(0); + const debounceTimeout = useRef(null); + + // -- HELPERS + // ---------------------------------- + + /** + * Filter an array of errors with specific validation type on them + * + * @param _errors The array of errors to filter + * @param validationType The validation type to filter out + */ + const filterErrors = ( + _errors: ValidationError[], + validationTypeToFilterOut: string | string[] = VALIDATION_TYPES.FIELD + ): ValidationError[] => { + const validationTypeToArray = Array.isArray(validationTypeToFilterOut) + ? (validationTypeToFilterOut as string[]) + : ([validationTypeToFilterOut] as string[]); + + return _errors.filter(error => + validationTypeToArray.every(_type => error.validationType !== _type) + ); + }; + + const runFormatters = (input: unknown): unknown => { + const isEmptyString = typeof input === 'string' && input.trim() === ''; + + if (isEmptyString) { + return input; + } + return formatters.reduce((output, formatter) => formatter(output), input); + }; + + const onValueChange = () => { + if (isPristine) { + setPristine(false); + } + setIsUpdating(true); + + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + debounceTimeout.current = setTimeout(() => { + setIsUpdating(false); + }, errorDisplayDelay); + }; + + const validateSync = ({ + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: unknown; + validationTypeToValidate?: string; + }): ValidationError[] => { + const validationErrors: ValidationError[] = []; + let skip = false; + + const runValidation = ({ + validator, + exitOnFail, + type: validationType = VALIDATION_TYPES.FIELD, + }: ValidationConfig) => { + if ( + skip || + (typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate) + ) { + return; + } + let validationResult; + + try { + validationResult = validator({ + value: (valueToValidate as unknown) as string, + errors: validationErrors, + form, + formData, + path, + }); + + if (validationResult && exitOnFail !== false) { + throw validationResult; + } + } catch (error) { + // If an error is thrown, skip the rest of the validations + skip = true; + validationResult = error; + } + + return validationResult; + }; + + // Execute each validations for the field sequentially + validations.forEach(validation => { + const validationResult = runValidation(validation); + + if (validationResult) { + validationErrors.push({ + ...validationResult, + validationType: validation.type || VALIDATION_TYPES.FIELD, + }); + } + }); + + return validationErrors; + }; + + const validateAsync = async ({ + formData, + value: valueToValidate, + validationTypeToValidate, + }: { + formData: any; + value: unknown; + validationTypeToValidate?: string; + }): Promise => { + const validationErrors: ValidationError[] = []; + let skip = false; + + // By default, for fields that have an asynchronous validation + // we will clear the errors as soon as the field value changes. + clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); + + const runValidation = async ({ + validator, + exitOnFail, + type: validationType = VALIDATION_TYPES.FIELD, + }: ValidationConfig) => { + if ( + skip || + (typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate) + ) { + return; + } + let validationResult; + + try { + validationResult = await validator({ + value: (valueToValidate as unknown) as string, + errors: validationErrors, + form, + formData, + path, + }); + + if (validationResult && exitOnFail !== false) { + throw validationResult; + } + } catch (error) { + // If an error is thrown, skip the rest of the validations + skip = true; + validationResult = error; + } + + return validationResult; + }; + + // Sequencially execute all the validations for the field + for (const validation of validations) { + const validationResult = await runValidation(validation); + + if (validationResult) { + validationErrors.push({ + ...validationResult, + validationType: validation.type || VALIDATION_TYPES.FIELD, + }); + } + } + + return validationErrors; + }; + + // -- API + // ---------------------------------- + const clearErrors: Field['clearErrors'] = (validationType = VALIDATION_TYPES.FIELD) => { + setErrors(previousErrors => filterErrors(previousErrors, validationType)); + }; + + /** + * Validate a form field, running all its validations. + * If a validationType is provided then only that validation will be executed, + * skipping the other type of validation that might exist. + */ + const validate: Field['validate'] = (validationData = {}) => { + const { + formData = form.getFormData({ unflatten: false }), + value: valueToValidate = value, + validationType, + } = validationData; + + setValidating(true); + + // By the time our validate function has reached completion, it’s possible + // that validate() will have been called again. If this is the case, we need + // to ignore the results of this invocation and only use the results of + // the most recent invocation to update the error state for a field + const validateIteration = ++validateCounter.current; + + const onValidationResult = (validationErrors: ValidationError[]): FieldValidateResponse => { + if (validateIteration === validateCounter.current) { + // This is the most recent invocation + setValidating(false); + // Update the errors array + setErrors(previousErrors => { + // First filter out the validation type we are currently validating + const filteredErrors = filterErrors(previousErrors, validationType); + return [...filteredErrors, ...validationErrors]; + }); + } + return { + isValid: validationErrors.length === 0, + errors: validationErrors, + }; + }; + + if (isValidationAsync) { + return validateAsync({ + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }).then(onValidationResult); + } else { + const validationErrors = validateSync({ + formData, + value: valueToValidate, + validationTypeToValidate: validationType, + }); + return onValidationResult(validationErrors); + } + }; + + /** + * Handler to change the field value + * + * @param newValue The new value to assign to the field + */ + const setValue: Field['setValue'] = newValue => { + onValueChange(); + + const formattedValue = runFormatters(newValue); + setStateValue(formattedValue); + + // Update the form data observable + form.__updateFormDataAt(path, getOutputValue(formattedValue)); + }; + + const _setErrors: Field['setErrors'] = _errors => { + setErrors(_errors.map(error => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); + }; + + /** + * Form "onChange" event handler + * + * @param event Form input change event + */ + const onChange: Field['onChange'] = event => { + const newValue = {}.hasOwnProperty.call(event!.target, 'checked') + ? event.target.checked + : event.target.value; + + setValue(newValue); + }; + + /** + * As we can have multiple validation types (FIELD, ASYNC, ARRAY_ITEM), this + * method allows us to retrieve error messages for certain types of validation. + * + * For example, if we want to validation error messages to be displayed when the user clicks the "save" button + * _but_ in case of an asynchronous validation (for ex. an HTTP request that would validate an index name) we + * want to immediately display the error message, we would have 2 types of validation: FIELD & ASYNC + * + * @param validationType The validation type to return error messages from + */ + const getErrorsMessages: Field['getErrorsMessages'] = (args = {}) => { + const { errorCode, validationType = VALIDATION_TYPES.FIELD } = args; + const errorMessages = errors.reduce((messages, error) => { + const isSameErrorCode = errorCode && error.code === errorCode; + const isSamevalidationType = + error.validationType === validationType || + (validationType === VALIDATION_TYPES.FIELD && + !{}.hasOwnProperty.call(error, 'validationType')); + + if (isSameErrorCode || (typeof errorCode === 'undefined' && isSamevalidationType)) { + return messages ? `${messages}, ${error.message}` : (error.message as string); + } + return messages; + }, ''); + + return errorMessages ? errorMessages : null; + }; + + const getOutputValue: Field['__getOutputValue'] = (rawValue = value) => serializer(rawValue); + + // -- EFFECTS + // ---------------------------------- + useEffect(() => { + if (isPristine) { + // Avoid validate on mount + return; + } + form.__validateFields(fieldsToValidateOnChange); + }, [value]); + + const field: Field = { + path, + type, + label, + helpText, + value, + errors, + form, + isPristine, + isValidating, + isUpdating, + onChange, + getErrorsMessages, + setValue, + setErrors: _setErrors, + clearErrors, + validate, + __getOutputValue: getOutputValue, + }; + + form.__addField(field); + + return field; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts new file mode 100644 index 0000000000000..04ac62a5fe8ec --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -0,0 +1,192 @@ +/* + * 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 { useState, useRef } from 'react'; + +import { Form, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; +import { getAt, mapFormFields, flattenObject, unflattenObject, Subject } from '../lib'; + +const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; + +export const useForm = ( + formConfig: FormConfig | undefined = {} +): { form: Form } => { + const { + onSubmit, + schema, + defaultValue = {}, + serializer = (data: any) => data, + deSerializer = (data: any) => data, + options = { errorDisplayDelay: DEFAULT_ERROR_DISPLAY_TIMEOUT, stripEmptyFields: true }, + } = formConfig; + const defaultValueDeSerialized = + Object.keys(defaultValue).length === 0 ? defaultValue : deSerializer(defaultValue); + const [isSubmitted, setSubmitted] = useState(false); + const [isSubmitting, setSubmitting] = useState(false); + const [isValid, setIsValid] = useState(true); + const fieldsRefs = useRef({}); + + // formData$ is an observable we can subscribe to in order to receive live + // update of the raw form data. As an observable it does not trigger any React + // render(). + // The component is the one in charge of reading this observable + // and updating its state to trigger the necessary view render. + const formData$ = useRef>(new Subject(flattenObject(defaultValue) as T)); + + // -- HELPERS + // ---------------------------------- + const fieldsToArray = () => Object.values(fieldsRefs.current); + + const stripEmptyFields = (fields: FieldsMap): FieldsMap => { + if (options.stripEmptyFields) { + return Object.entries(fields).reduce( + (acc, [key, field]) => { + if (field.value !== '') { + acc[key] = field; + } + return acc; + }, + {} as FieldsMap + ); + } + return fields; + }; + + // -- API + // ---------------------------------- + const getFormData: Form['getFormData'] = (getDataOptions = { unflatten: true }) => + getDataOptions.unflatten + ? (unflattenObject( + mapFormFields(stripEmptyFields(fieldsRefs.current), field => field.__getOutputValue()) + ) as T) + : Object.entries(fieldsRefs.current).reduce( + (acc, [key, field]) => ({ + ...acc, + [key]: field.__getOutputValue(), + }), + {} as T + ); + + const updateFormDataAt: Form['__updateFormDataAt'] = (path, value) => { + const currentFormData = formData$.current.value; + formData$.current.next({ ...currentFormData, [path]: value }); + return formData$.current.value; + }; + + const validateFields: Form['__validateFields'] = async fieldNames => { + const fieldsToValidate = fieldNames + ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined) + : fieldsToArray().filter(field => field.isPristine); + + const formData = getFormData({ unflatten: false }); + + await Promise.all(fieldsToValidate.map(field => field.validate({ formData }))); + + const isFormValid = fieldsToArray().every( + field => field.getErrorsMessages() === null && !field.isValidating + ); + setIsValid(isFormValid); + + return isFormValid; + }; + + const addField: Form['__addField'] = field => { + fieldsRefs.current[field.path] = field; + + // Only update the formData if the path does not exist (it is the _first_ time + // the field is added), to avoid entering an infinite loop when the form is re-rendered. + if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) { + updateFormDataAt(field.path, field.__getOutputValue()); + } + }; + + const removeField: Form['__removeField'] = _fieldNames => { + const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; + const currentFormData = { ...formData$.current.value } as FormData; + + fieldNames.forEach(name => { + delete fieldsRefs.current[name]; + delete currentFormData[name]; + }); + + formData$.current.next(currentFormData as T); + }; + + const setFieldValue: Form['setFieldValue'] = (fieldName, value) => { + fieldsRefs.current[fieldName].setValue(value); + }; + + const setFieldErrors: Form['setFieldErrors'] = (fieldName, errors) => { + fieldsRefs.current[fieldName].setErrors(errors); + }; + + const getFields: Form['getFields'] = () => fieldsRefs.current; + + const getFieldDefaultValue: Form['getFieldDefaultValue'] = fieldName => + getAt(fieldName, defaultValueDeSerialized, false); + + const readFieldConfigFromSchema: Form['__readFieldConfigFromSchema'] = fieldName => { + const config = (getAt(fieldName, schema ? schema : {}, false) as FieldConfig) || {}; + + return config; + }; + + const onSubmitForm: Form['onSubmit'] = async e => { + if (e) { + e.preventDefault(); + } + + setSubmitting(true); + setSubmitted(true); // User has attempted to submit the form at least once + + const isFormValid = await validateFields(); + const formData = serializer(getFormData() as T); + + if (onSubmit) { + await onSubmit(formData, isFormValid); + } + + setSubmitting(false); + + return { data: formData, isValid: isFormValid }; + }; + + const form: Form = { + isSubmitted, + isSubmitting, + isValid, + options, + onSubmit: onSubmitForm, + setFieldValue, + setFieldErrors, + getFields, + getFormData, + getFieldDefaultValue, + __formData$: formData$, + __updateFormDataAt: updateFormDataAt, + __readFieldConfigFromSchema: readFieldConfigFromSchema, + __addField: addField, + __removeField: removeField, + __validateFields: validateFields, + }; + + return { + form, + }; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts new file mode 100644 index 0000000000000..68c7cba15931a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts @@ -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. + */ + +// Only export the useForm hook. The "useField" hook is for internal use +// as the consumer of the library must use the component +export { useForm } from './hooks'; +export { fieldFormatters } from './lib'; + +export * from './components'; +export * from './constants'; +export * from './types'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts new file mode 100644 index 0000000000000..830c7725c95f4 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { Subject, Subscription } from './subject'; +export * from './utils'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts new file mode 100644 index 0000000000000..8ccc67a133355 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +type Listener = (value: T) => void; + +export interface Subscription { + unsubscribe: () => void; +} + +export class Subject { + private callbacks: Set> = new Set(); + value: T; + + constructor(value: T) { + this.value = value; + } + + subscribe(fn: Listener): Subscription { + this.callbacks.add(fn); + + setTimeout(() => { + fn(this.value); + }); + + const unsubscribe = () => this.callbacks.delete(fn); + return { + unsubscribe, + }; + } + + next(value: T) { + this.value = value; + this.callbacks.forEach(fn => fn(value)); + } +} diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts new file mode 100644 index 0000000000000..7342b217abc62 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -0,0 +1,116 @@ +/* + * 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 { Field } from '../types'; + +const numRegEx = /^\d+$/; + +const isNumber = (val: string) => numRegEx.test(val); + +export const getAt = (path: string, object: any, throwIfNotFound = true): unknown => { + const pathToArray = path.split('.'); + const value = object[pathToArray[0]]; + + if (pathToArray.length === 1) { + return value; + } + + if (value !== null && typeof value === 'object') { + return getAt(pathToArray.slice(1).join('.'), value, throwIfNotFound); + } + + if (throwIfNotFound) { + throw new Error(`Can't access path "${path}" on ${JSON.stringify(object)}`); + } + + return undefined; +}; + +const setAt = (path: string, object: any, value: unknown, createUnknownPath = true): any => { + const pathToArray = path.split('.'); + + if (pathToArray.length === 1) { + object[pathToArray[0]] = value; + return object; + } + + let target = object; + + pathToArray.slice(0, -1).forEach((key, i) => { + if (!{}.hasOwnProperty.call(target, key)) { + if (createUnknownPath) { + // If the path segment is a number, we create an Array + // otherwise we create an object. + target[key] = isNumber(pathToArray[i + 1]) ? [] : {}; + } else { + throw new Error(`Can't set value "${value}" at "${path}" on ${JSON.stringify(object)}`); + } + } + + target = target[key]; + + if (target === null || (typeof target !== 'object' && !Array.isArray(target))) { + throw new Error( + `Can't set value "${value}" on a primitive. Path provided: "${path}", target: ${JSON.stringify( + object + )}` + ); + } + }); + + const keyToSet = pathToArray[pathToArray.length - 1]; + target[keyToSet] = value; + + return object; +}; + +export const unflattenObject = (object: any) => + Object.entries(object).reduce((acc, [key, field]) => { + setAt(key, acc, field); + return acc; + }, {}); + +export const flattenObject = ( + object: Record, + to: Record = {}, + paths: string[] = [] +): Record => + Object.entries(object).reduce((acc, [key, value]) => { + const updatedPaths = [...paths, key]; + if (value !== null && typeof value === 'object') { + return flattenObject(value, to, updatedPaths); + } + acc[updatedPaths.join('.')] = value; + return acc; + }, to); + +/** + * Helper to map the object of fields to any of its value + * + * @param formFields key value pair of path and form Fields + * @param fn Iterator function to execute on the field + */ +export const mapFormFields = (formFields: Record, fn: (field: Field) => any) => + Object.entries(formFields).reduce( + (acc, [key, field]) => { + acc[key] = fn(field); + return acc; + }, + {} as Record + ); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts new file mode 100644 index 0000000000000..2680b27ccca0b --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts @@ -0,0 +1,149 @@ +/* + * 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 { ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react'; +import { Subject } from './lib'; + +export interface Form { + readonly isSubmitted: boolean; + readonly isSubmitting: boolean; + readonly isValid: boolean; + readonly options: FormOptions; + onSubmit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + setFieldValue: (fieldName: string, value: FieldValue) => void; + setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; + getFields: () => FieldsMap; + getFormData: (options?: { unflatten?: boolean }) => T; + getFieldDefaultValue: (fieldName: string) => unknown; + readonly __formData$: MutableRefObject>; + __addField: (field: Field) => void; + __removeField: (fieldNames: string | string[]) => void; + __validateFields: (fieldNames?: string[]) => Promise; + __updateFormDataAt: (field: string, value: unknown) => T; + __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; +} + +export interface FormSchema { + [key: string]: FormSchemaEntry; +} +type FormSchemaEntry = + | FieldConfig + | Array> + | { [key: string]: FieldConfig | Array> | FormSchemaEntry }; + +export interface FormConfig { + onSubmit?: (data: T, isFormValid: boolean) => void; + schema?: FormSchema; + defaultValue?: Partial; + serializer?: SerializerFunc; + deSerializer?: SerializerFunc; + options?: FormOptions; +} + +export interface FormOptions { + errorDisplayDelay: number; + /** + * Remove empty string field ("") from form data + */ + stripEmptyFields: boolean; +} + +export interface Field { + readonly path: string; + readonly label?: string; + readonly helpText?: string; + readonly type: string; + readonly value: unknown; + readonly errors: ValidationError[]; + readonly isPristine: boolean; + readonly isValidating: boolean; + readonly isUpdating: boolean; + readonly form: Form; + getErrorsMessages: (args?: { + validationType?: 'field' | string; + errorCode?: string; + }) => string | null; + onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; + setValue: (value: FieldValue) => void; + setErrors: (errors: ValidationError[]) => void; + clearErrors: (type?: string | string[]) => void; + validate: (validateData?: { + formData?: any; + value?: unknown; + validationType?: string; + }) => FieldValidateResponse | Promise; + __getOutputValue: (rawValue?: unknown) => unknown; +} + +export interface FieldConfig { + readonly path?: string; + readonly label?: string; + readonly helpText?: string; + readonly type?: HTMLInputElement['type']; + readonly defaultValue?: unknown; + readonly validations?: Array>; + readonly formatters?: FormatterFunc[]; + readonly deSerializer?: SerializerFunc; + readonly serializer?: SerializerFunc; + readonly fieldsToValidateOnChange?: string[]; + readonly isValidationAsync?: boolean; + readonly errorDisplayDelay?: number; +} + +export interface FieldsMap { + [key: string]: Field; +} + +export type FormSubmitHandler = (formData: T, isValid: boolean) => Promise; + +export interface ValidationError { + message: string | ((error: ValidationError) => string); + code?: string; + validationType?: string; + [key: string]: any; +} + +export type ValidationFunc = (data: { + path: string; + value: unknown; + form: Form; + formData: T; + errors: readonly ValidationError[]; +}) => ValidationError | void | undefined | Promise; + +export interface FieldValidateResponse { + isValid: boolean; + errors: ValidationError[]; +} + +export type SerializerFunc = (value: any) => T; + +export type FormData = Record; + +type FormatterFunc = (value: any) => unknown; + +// We set it as unknown as a form field can be any of any type +// string | number | boolean | string[] ... +type FieldValue = unknown; + +export interface ValidationConfig { + validator: ValidationFunc; + type?: string; + exitOnFail?: boolean; +} diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts new file mode 100644 index 0000000000000..a33b03853aeab --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts @@ -0,0 +1,36 @@ +/* + * 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; + +// This deSerializer takes the previously selected options and map them +// against the default select options values. +export const multiSelectSelectedValueToOptions: FuncType = selectOptions => 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, + })); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts new file mode 100644 index 0000000000000..603a5d7e77777 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts @@ -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); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts new file mode 100644 index 0000000000000..e6beb012d1e14 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts @@ -0,0 +1,41 @@ +/* + * 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 { containsChars } from '../../../validators/string'; +import { formatError } from '../../errors'; + +export const containsCharsField = ({ + message, + chars, +}: { + message: string; + chars: string | string[]; +}) => (...args: Parameters): ReturnType => { + const [{ value }] = args; + + const { doesContain, charsFound } = containsChars(chars)(value as string); + + if (doesContain) { + return { + ...formatError('CONTAINS_INVALID_CHARS', message), + charsFound, + }; + } +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts new file mode 100644 index 0000000000000..fccf28c8e0b4d --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts @@ -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 { fieldMissingError } from '../../errors'; + +export const emptyField = (message: string) => ( + ...args: Parameters +): ReturnType => { + const [{ value, path }] = args; + + if (typeof value === 'string') { + return isEmptyString(value) ? { ...fieldMissingError(path), message } : undefined; + } + + if (Array.isArray(value)) { + return isEmptyArray(value) ? { ...fieldMissingError(path), message } : undefined; + } +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts new file mode 100644 index 0000000000000..ee47ae7b6cc07 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts @@ -0,0 +1,25 @@ +/* + * 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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts new file mode 100644 index 0000000000000..a70ec0d3036f0 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts @@ -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. + */ + +// 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 { formatError } from '../../errors'; + +const INDEX_ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', '*']; + +export const indexNameField = (...args: Parameters): ReturnType => { + const [{ value }] = args; + + if (startsWith('.')(value as string)) { + return formatError('INDEX_NAME', 'Cannot start with a dot (".").'); + } + + const { doesContain: doesContainSpaces } = containsChars(' ')(value as string); + if (doesContainSpaces) { + return formatError('INDEX_NAME', 'Cannot contain spaces.'); + } + + const { charsFound, doesContain } = containsChars(INDEX_ILLEGAL_CHARACTERS)(value as string); + if (doesContain) { + return formatError( + 'INDEX_NAME', + `Cannot contain the following characters: "${charsFound.join(',')}."` + ); + } +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts new file mode 100644 index 0000000000000..3f6aced2a81a7 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts @@ -0,0 +1,34 @@ +/* + * 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 { hasMinLengthString } from '../../../validators/string'; +import { hasMinLengthArray } from '../../../validators/array'; +import { minLengthError } from '../../errors'; + +export const minLengthField = (length = 0) => ( + ...args: Parameters +): ReturnType => { + const [{ value }] = args; + + if (Array.isArray(value)) { + return hasMinLengthArray(length)(value) ? undefined : minLengthError(length); + } + return hasMinLengthString(length)((value as string).trim()) ? undefined : minLengthError(length); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts new file mode 100644 index 0000000000000..23267f49cc22b --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts @@ -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 { ValidationFunc } from '../../hook_form_lib'; +import { hasMinLengthArray } from '../../../validators/array'; +import { minSelectionError } from '../../errors'; +import { multiSelectOptionsToSelectedValue } from '../../lib'; + +/** + * Validator to validate that a EuiSelectable has a minimum number + * of items selected. + * @param total Minimum number of items + */ +export const minSelectableSelectionField = (total = 0) => ( + ...args: Parameters +): ReturnType => { + const [{ value }] = args; + + return hasMinLengthArray(total)(multiSelectOptionsToSelectedValue(value as Option[])) + ? undefined + : minSelectionError(total); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts new file mode 100644 index 0000000000000..2fdb173a41e67 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts @@ -0,0 +1,38 @@ +/* + * 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, isUrl } from '../../../validators/string'; +import { formatError } from '../../errors'; + +export const urlField = (allowEmpty = false) => ( + ...args: Parameters +): ReturnType => { + const [{ value }] = args; + + if (typeof value !== 'string') { + return formatError('URL'); + } + + if (allowEmpty && isEmptyString(value)) { + return; + } + + return isUrl(value) ? undefined : formatError('URL'); +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts new file mode 100644 index 0000000000000..9af6b57ba3bef --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts @@ -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 './de_serializers'; +export * from './serializers'; +export * from './field_formatters'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts new file mode 100644 index 0000000000000..3d4517d65923c --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts @@ -0,0 +1,49 @@ +/* + * 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'; + +/** + * Return an array of labels of all the options that are selected + * + * @param value The Eui Selectable options array + */ +export const multiSelectOptionsToSelectedValue: SerializerFunc = ( + options: Option[] +): string[] => options.filter(option => option.checked === 'on').map(option => option.label); diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts new file mode 100644 index 0000000000000..a03b307427b4d --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts @@ -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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts new file mode 100644 index 0000000000000..aaa8b810bed1a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts @@ -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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts new file mode 100644 index 0000000000000..cb724e048c9e0 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts @@ -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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts new file mode 100644 index 0000000000000..f97caeb9d4e4c --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts @@ -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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts new file mode 100644 index 0000000000000..869a2477cfd4b --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts @@ -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, + }; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts new file mode 100644 index 0000000000000..58ba1ccfdc388 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts @@ -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); diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts new file mode 100644 index 0000000000000..371ccf36e8151 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts @@ -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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts new file mode 100644 index 0000000000000..bc12277c68284 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts @@ -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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts new file mode 100644 index 0000000000000..4c34ebbb4886d --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts @@ -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 './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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts new file mode 100644 index 0000000000000..b82eda817b451 --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts @@ -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() === ''; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts new file mode 100644 index 0000000000000..5954760bf106a --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts @@ -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; +}; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts new file mode 100644 index 0000000000000..ffd840a07eebe --- /dev/null +++ b/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts @@ -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); diff --git a/x-pack/legacy/plugins/index_management/public/app.js b/x-pack/legacy/plugins/index_management/public/app.js index 07fa15a1a1332..0b8c6be0e7929 100644 --- a/x-pack/legacy/plugins/index_management/public/app.js +++ b/x-pack/legacy/plugins/index_management/public/app.js @@ -12,6 +12,7 @@ import { TemplateCreate } from './sections/template_create'; import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { trackUiMetric } from './services'; +import { CreateIndex } from './sections/create_index'; export const App = () => { useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); @@ -26,6 +27,7 @@ export const App = () => { // Export this so we can test it with a different router. export const AppWithoutRouter = () => ( + Promise<{ isValid: boolean; data: Mappings }>; + +const initialData = { + dynamic: 'strict', + date_detection: false, + numeric_detection: true, + dynamic_date_formats: ['MM/dd/yyyy'], + properties: { + title: { + type: 'text', + store: true, + index: false, + fielddata: true, + }, + someObject: { + type: 'object', + store: true, + index: true, + fielddata: true, + properties: { + title: { + type: 'text', + store: true, + index: false, + fielddata: true, + }, + myDate: { + type: 'date', + store: true, + }, + superNested: { + type: 'object', + store: true, + index: true, + fielddata: true, + properties: { + lastName: { + type: 'text', + store: true, + index: false, + fielddata: true, + }, + name: { + type: 'text', + store: true, + index: true, + fielddata: true, + }, + }, + }, + }, + }, + someKeyword: { + type: 'text', + store: true, + index: false, + fielddata: true, + }, + }, +}; + +export const CreateIndex = () => { + const getMappingsEditorData = useRef(() => + Promise.resolve({ + isValid: true, + data: {}, + }) + ); + const [isFormSubmitted, setIsFormSubmitted] = useState(false); + const [mappings, setMappings] = useState(initialData); + const [mappingsState, setMappingsState] = useState>({ + isValid: true, + isEditingProperty: false, + }); + + const setGetMappingsEditorDataHandler = (handler: GetMappingsEditorDataHandler) => + (getMappingsEditorData.current = handler); + + const onClick = async () => { + const { isValid, data } = await getMappingsEditorData.current(); + // eslint-disable-next-line no-console + console.log(isValid, data); + setMappingsState(prev => ({ ...prev, isValid })); + setMappings(data); + setIsFormSubmitted(true); + }; + + return ( + + +

          Index Mappings...

          +
          + + + + + +
          + +

          + Everything below is OUTSIDE the MappingsEditor +

          + + {isFormSubmitted && mappingsState.isEditingProperty && ( + +

          + Warning: You are in the middle of editing a property. Please save the property before + continuing. +

          + +
          + )} + + Send form + + + + +

          Mappings editor data:

          +
          + + + {mappingsState.isValid ? ( +
          +          {JSON.stringify(mappings, null, 2)}
          +        
          + ) : ( +
          The mappings JSON data is not valid.
          + )} +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts b/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts new file mode 100644 index 0000000000000..2b556147894da --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './create_index'; From 67b3a116765f9aac092c700beef01577090aa399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 19 Aug 2019 14:55:24 +0200 Subject: [PATCH 32/64] Revert "Add Hook form lib (To be reverted)" This reverts commit 9befa252bde305394124709ede17a1561062596c. --- .../static/forms/components/field.tsx | 210 ---------- .../static/forms/components/form_row.tsx | 54 --- .../static/forms/components/index.ts | 21 - .../static/forms/errors/constants.ts | 24 -- .../static/forms/errors/errors.ts | 55 --- .../static/forms/errors/index.ts | 21 - .../components/form_data_provider.ts | 65 --- .../forms/hook_form_lib/components/index.ts | 22 - .../hook_form_lib/components/use_array.ts | 87 ---- .../hook_form_lib/components/use_field.tsx | 88 ---- .../static/forms/hook_form_lib/constants.ts | 36 -- .../static/forms/hook_form_lib/hooks/index.ts | 21 - .../forms/hook_form_lib/hooks/use_field.ts | 385 ------------------ .../forms/hook_form_lib/hooks/use_form.ts | 192 --------- .../static/forms/hook_form_lib/index.ts | 27 -- .../static/forms/hook_form_lib/lib/index.ts | 21 - .../static/forms/hook_form_lib/lib/subject.ts | 51 --- .../static/forms/hook_form_lib/lib/utils.ts | 116 ------ .../static/forms/hook_form_lib/types.ts | 149 ------- .../static/forms/lib/de_serializers.ts | 36 -- .../static/forms/lib/field_formatters.ts | 28 -- .../lib/field_validators/contains_char.ts | 41 -- .../forms/lib/field_validators/empty_field.ts | 37 -- .../forms/lib/field_validators/index.ts | 25 -- .../forms/lib/field_validators/index_name.ts | 47 --- .../forms/lib/field_validators/min_length.ts | 34 -- .../min_selectable_selection.ts | 40 -- .../static/forms/lib/field_validators/url.ts | 38 -- .../static/forms/lib/index.ts | 22 - .../static/forms/lib/serializers.ts | 49 --- .../static/validators/array/has_max_length.ts | 20 - .../static/validators/array/has_min_length.ts | 20 - .../static/validators/array/index.ts | 22 - .../static/validators/array/is_empty.ts | 20 - .../validators/string/contains_chars.ts | 32 -- .../static/validators/string/ends_with.ts | 20 - .../validators/string/has_max_length.ts | 20 - .../validators/string/has_min_length.ts | 20 - .../static/validators/string/index.ts | 26 -- .../static/validators/string/is_empty.ts | 20 - .../static/validators/string/is_url.ts | 47 --- .../static/validators/string/starts_with.ts | 20 - .../plugins/index_management/public/app.js | 2 - .../sections/create_index/create_index.tsx | 152 ------- .../public/sections/create_index/index.ts | 7 - 45 files changed, 2490 deletions(-) delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts delete mode 100644 src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts delete mode 100644 x-pack/legacy/plugins/index_management/public/sections/create_index/create_index.tsx delete mode 100644 x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx deleted file mode 100644 index 01ee046757dcc..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/components/field.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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, - EuiFieldNumber, - EuiSelect, - EuiComboBox, - EuiSwitch, - EuiCheckbox, - EuiSelectable, - EuiPanel, - EuiComboBoxOptionProps, -} from '@elastic/eui'; -import uuid from 'uuid'; - -import { - Field as FieldType, - FIELD_TYPES, - VALIDATION_TYPES, - FieldValidateResponse, -} from '../hook_form_lib'; - -interface Props { - field: FieldType; - fieldProps?: Record; -} - -export const Field = ({ field, fieldProps = {} }: Props) => { - let isInvalid: boolean; - let errorMessage: string | null; - - if (field.type === FIELD_TYPES.COMBO_BOX) { - // Errors for the comboBox value (the "array") - const errorMessageField = field.form.isSubmitted ? field.getErrorsMessages() : null; - - // Errors for comboBox option added (the array "item") - const errorMessageArrayItem = field.getErrorsMessages({ - validationType: VALIDATION_TYPES.ARRAY_ITEM, - }); - - isInvalid = field.errors.length - ? field.form.isSubmitted || errorMessageArrayItem !== null - : false; - - // Concatenate error messages. - errorMessage = - errorMessageField && errorMessageArrayItem - ? `${errorMessageField}, ${errorMessageArrayItem}` - : errorMessageField - ? errorMessageField - : errorMessageArrayItem; - } else { - isInvalid = !field.isUpdating && (field.form.isSubmitted && field.errors.length > 0); - errorMessage = - !field.isUpdating && field.errors.length ? (field.errors[0].message as string) : null; - } - - const onCreateComboOption = async (value: string) => { - // Note: for now, we assume that all validations for a comboBox array item are synchronous - // If there is a need to support asynchronous validation, we'll work on it. - const { isValid } = field.validate({ - value, - validationType: VALIDATION_TYPES.ARRAY_ITEM, - }) as FieldValidateResponse; - - if (!isValid) { - field.setValue(field.value as string[]); - return; - } - - 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); - } - }; - - const doDisplayLabelOnTop = - field.type !== FIELD_TYPES.TOGGLE && field.type !== FIELD_TYPES.CHECKBOX; - - const renderField = () => { - switch (field.type) { - case FIELD_TYPES.NUMBER: - return ( - - ); - case FIELD_TYPES.SELECT: - return ( - { - field.setValue(e.target.value); - }} - hasNoInitialSelection={true} - isInvalid={false} - {...(fieldProps as { options: any; [key: string]: any })} - /> - ); - case FIELD_TYPES.COMBO_BOX: - return ( - ({ label: v }))} - onCreateOption={onCreateComboOption} - onChange={onComboChange} - onSearchChange={onSearchComboChange} - fullWidth - {...fieldProps} - /> - ); - case FIELD_TYPES.TOGGLE: - return ( - - ); - case FIELD_TYPES.CHECKBOX: - return ( - - ); - case FIELD_TYPES.MULTI_SELECT: - return ( - { - field.setValue(options); - }} - options={field.value as any[]} - {...fieldProps} - > - {(list, search) => ( - - {search} - {list} - - )} - - ); - default: - return ( - - ); - } - }; - - return ( - - {renderField()} - - ); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx deleted file mode 100644 index 7eb3e465e315a..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/components/form_row.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { Field as FieldType } from '../hook_form_lib'; -import { Field } from './field'; - -interface Props { - title: string | JSX.Element; - description?: string | JSX.Element; - field?: FieldType; - fieldProps?: Record; - children?: React.ReactNode; -} - -export const FormRow = ({ title, description, field, fieldProps = {}, children }: Props) => { - // If a string is provided, create a default Euititle of size "m" - const _title = - typeof title === 'string' ? ( - -

          {title}

          -
          - ) : ( - title - ); - - if (!children && !field) { - throw new Error('You need to provide either children or a field to the FormRow'); - } - - return ( - - {children ? children : } - - ); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts deleted file mode 100644 index de9e85ebf2e24..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/components/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts deleted file mode 100644 index 87dd6c95c8840..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/errors/constants.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -// Error codes -export const ERR_FIELD_MISSING = 'ERR_FIELD_MISSING'; -export const ERR_MIN_LENGTH = 'ERR_MIN_LENGTH'; -export const ERR_MIN_SELECTION = 'ERR_MIN_SELECTION'; -export const ERR_FIELD_FORMAT = 'ERR_FIELD_FORMAT'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts deleted file mode 100644 index 16f56fbfd2d75..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/errors/errors.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { - ERR_FIELD_FORMAT, - ERR_FIELD_MISSING, - ERR_MIN_LENGTH, - ERR_MIN_SELECTION, -} from './constants'; - -export const fieldMissingError = (fieldName: string, message = 'Field missing') => ({ - code: ERR_FIELD_MISSING, - fieldName, - message, -}); - -export const minLengthError = ( - length: number, - message = (error: any) => `Must have a minimun length of ${error.length}.` -) => ({ - code: ERR_MIN_LENGTH, - length, - message, -}); - -export const minSelectionError = ( - length: number, - message = (error: any) => `Must select at least ${error.length} items.` -) => ({ - code: ERR_MIN_SELECTION, - length, - message, -}); - -export const formatError = (format: string, message = 'Format error') => ({ - code: ERR_FIELD_FORMAT, - format, - message, -}); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts deleted file mode 100644 index 1fda395a4d7e5..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/errors/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 './errors'; -export * from './constants'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts deleted file mode 100644 index 861ae32e01875..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { useState, useEffect, useRef } from 'react'; - -import { Form, FormData } from '../types'; -import { Subscription } from '../lib'; - -interface Props { - children: (formData: FormData) => JSX.Element | null; - form: Form; - pathsToWatch?: string | string[]; -} - -export const FormDataProvider = ({ children, form, pathsToWatch }: Props) => { - const [formData, setFormData] = useState({}); - const previousState = useRef({}); - const subscription = useRef(null); - - useEffect(() => { - let didUnsubscribe = false; - subscription.current = form.__formData$.current.subscribe(data => { - if (didUnsubscribe) { - return; - } - // To avoid re-rendering the children for updates on the form data - // that we are **not** interested in, we can specify one or multiple path(s) - // to watch. - if (pathsToWatch) { - const valuesToWatchToArray = Array.isArray(pathsToWatch) - ? (pathsToWatch as string[]) - : ([pathsToWatch] as string[]); - if (valuesToWatchToArray.some(value => previousState.current[value] !== data[value])) { - previousState.current = data; - setFormData(data); - } - } else { - setFormData(data); - } - }); - - return () => { - didUnsubscribe = true; - subscription.current!.unsubscribe(); - }; - }, [pathsToWatch]); - - return children(formData); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts deleted file mode 100644 index 307b71f8d86b4..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 './use_field'; -export * from './use_array'; -export * from './form_data_provider'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts deleted file mode 100644 index be9c90484e5d4..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 { useState, useRef } from 'react'; -import { Form } from '../types'; - -interface Props { - path: string; - form: Form; - initialNumberOfItems?: number; - children: (args: { - items: ArrayItem[]; - addItem: () => void; - removeItem: (id: number) => void; - }) => JSX.Element; -} - -export interface ArrayItem { - id: number; - path: string; - isNew: boolean; -} - -export const UseArray = ({ path, form, initialNumberOfItems, children }: Props) => { - const defaultValues = form.getFieldDefaultValue(path) as any[]; - const uniqueId = useRef(0); - - const getInitialRowsFromValues = (values: any[]): ArrayItem[] => - values.map((_, index) => ({ - id: uniqueId.current++, - path: `${path}.${index}`, - isNew: false, - })); - - const getNewItemAtIndex = (index: number): ArrayItem => ({ - id: uniqueId.current++, - path: `${path}.${index}`, - isNew: true, - }); - - const initialState = defaultValues - ? getInitialRowsFromValues(defaultValues) - : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); - - const [items, setItems] = useState(initialState); - - const updatePaths = (_rows: ArrayItem[]) => - _rows.map( - (row, index) => - ({ - ...row, - path: `${path}.${index}`, - } as ArrayItem) - ); - - const addItem = () => { - setItems(previousItems => { - const itemIndex = previousItems.length; - return [...previousItems, getNewItemAtIndex(itemIndex)]; - }); - }; - - const removeItem = (id: number) => { - setItems(previousItems => { - const updatedItems = previousItems.filter(item => item.id !== id); - return updatePaths(updatedItems); - }); - }; - - return children({ items, addItem, removeItem }); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx deleted file mode 100644 index 761d6c9fe9736..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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, { useEffect } from 'react'; - -import { Form, Field as FieldType, FieldConfig } from '../types'; -import { useField } from '../hooks'; - -interface Props { - path: string; - config?: FieldConfig; - defaultValue?: unknown; - form: Form; - component?: (({ field }: { field: FieldType } & any) => JSX.Element) | 'input'; - componentProps?: any; - children?: (field: FieldType) => JSX.Element; -} - -export const UseField = ({ - path, - config, - form, - defaultValue = form.getFieldDefaultValue(path), - component = 'input', - componentProps = {}, - children, -}: Props) => { - if (!config) { - config = form.__readFieldConfigFromSchema(path); - } - - // Don't modify the config object - const configCopy = - typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config }; - - if (!configCopy.path) { - configCopy.path = path; - } else { - if (configCopy.path !== path) { - throw new Error( - `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".` - ); - } - } - - const field = useField(form, path, configCopy); - - // Remove field from form when it is unmounted or if its path changes - useEffect(() => { - return () => { - form.__removeField(path); - }; - }, [path]); - - // Children prevails over anything else provided. - if (children) { - return children!(field); - } - - if (component === 'input') { - return ( - - ); - } - - return component({ field, ...componentProps }); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts deleted file mode 100644 index 89ea91485a2d0..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/constants.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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. - */ - -// Field types -export const FIELD_TYPES = { - TEXT: 'text', - NUMBER: 'number', - TOGGLE: 'toggle', - CHECKBOX: 'checkbox', - COMBO_BOX: 'comboBox', - SELECT: 'select', - MULTI_SELECT: 'multiSelect', -}; - -// Validation types -export const VALIDATION_TYPES = { - FIELD: 'field', // Default validation error (on the field value) - ASYNC: 'async', // Throw from asynchronous validations - ARRAY_ITEM: 'arrayItem', // If the field value is an Array, this error would be thrown if an _item_ of the array is invalid -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts deleted file mode 100644 index 6a04a592227f9..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { useField } from './use_field'; -export { useForm } from './use_form'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts deleted file mode 100644 index b2bac5da64333..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ /dev/null @@ -1,385 +0,0 @@ -/* - * 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 { useState, useEffect, useRef } from 'react'; - -import { - Form, - Field, - FieldConfig, - FieldValidateResponse, - ValidationConfig, - ValidationError, -} from '../types'; -import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; - -export const useField = (form: Form, path: string, config: FieldConfig = {}) => { - const { - type = FIELD_TYPES.TEXT, - defaultValue = '', - label = '', - helpText = '', - validations = [], - formatters = [], - fieldsToValidateOnChange = [path], - isValidationAsync = false, - errorDisplayDelay = form.options.errorDisplayDelay, - serializer = (value: unknown) => value, - deSerializer = (value: unknown) => value, - } = config; - - const [value, setStateValue] = useState( - typeof defaultValue === 'function' ? deSerializer(defaultValue()) : deSerializer(defaultValue) - ); - const [errors, setErrors] = useState([]); - const [isPristine, setPristine] = useState(true); - const [isValidating, setValidating] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - const validateCounter = useRef(0); - const debounceTimeout = useRef(null); - - // -- HELPERS - // ---------------------------------- - - /** - * Filter an array of errors with specific validation type on them - * - * @param _errors The array of errors to filter - * @param validationType The validation type to filter out - */ - const filterErrors = ( - _errors: ValidationError[], - validationTypeToFilterOut: string | string[] = VALIDATION_TYPES.FIELD - ): ValidationError[] => { - const validationTypeToArray = Array.isArray(validationTypeToFilterOut) - ? (validationTypeToFilterOut as string[]) - : ([validationTypeToFilterOut] as string[]); - - return _errors.filter(error => - validationTypeToArray.every(_type => error.validationType !== _type) - ); - }; - - const runFormatters = (input: unknown): unknown => { - const isEmptyString = typeof input === 'string' && input.trim() === ''; - - if (isEmptyString) { - return input; - } - return formatters.reduce((output, formatter) => formatter(output), input); - }; - - const onValueChange = () => { - if (isPristine) { - setPristine(false); - } - setIsUpdating(true); - - if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); - } - - debounceTimeout.current = setTimeout(() => { - setIsUpdating(false); - }, errorDisplayDelay); - }; - - const validateSync = ({ - formData, - value: valueToValidate, - validationTypeToValidate, - }: { - formData: any; - value: unknown; - validationTypeToValidate?: string; - }): ValidationError[] => { - const validationErrors: ValidationError[] = []; - let skip = false; - - const runValidation = ({ - validator, - exitOnFail, - type: validationType = VALIDATION_TYPES.FIELD, - }: ValidationConfig) => { - if ( - skip || - (typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate) - ) { - return; - } - let validationResult; - - try { - validationResult = validator({ - value: (valueToValidate as unknown) as string, - errors: validationErrors, - form, - formData, - path, - }); - - if (validationResult && exitOnFail !== false) { - throw validationResult; - } - } catch (error) { - // If an error is thrown, skip the rest of the validations - skip = true; - validationResult = error; - } - - return validationResult; - }; - - // Execute each validations for the field sequentially - validations.forEach(validation => { - const validationResult = runValidation(validation); - - if (validationResult) { - validationErrors.push({ - ...validationResult, - validationType: validation.type || VALIDATION_TYPES.FIELD, - }); - } - }); - - return validationErrors; - }; - - const validateAsync = async ({ - formData, - value: valueToValidate, - validationTypeToValidate, - }: { - formData: any; - value: unknown; - validationTypeToValidate?: string; - }): Promise => { - const validationErrors: ValidationError[] = []; - let skip = false; - - // By default, for fields that have an asynchronous validation - // we will clear the errors as soon as the field value changes. - clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); - - const runValidation = async ({ - validator, - exitOnFail, - type: validationType = VALIDATION_TYPES.FIELD, - }: ValidationConfig) => { - if ( - skip || - (typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate) - ) { - return; - } - let validationResult; - - try { - validationResult = await validator({ - value: (valueToValidate as unknown) as string, - errors: validationErrors, - form, - formData, - path, - }); - - if (validationResult && exitOnFail !== false) { - throw validationResult; - } - } catch (error) { - // If an error is thrown, skip the rest of the validations - skip = true; - validationResult = error; - } - - return validationResult; - }; - - // Sequencially execute all the validations for the field - for (const validation of validations) { - const validationResult = await runValidation(validation); - - if (validationResult) { - validationErrors.push({ - ...validationResult, - validationType: validation.type || VALIDATION_TYPES.FIELD, - }); - } - } - - return validationErrors; - }; - - // -- API - // ---------------------------------- - const clearErrors: Field['clearErrors'] = (validationType = VALIDATION_TYPES.FIELD) => { - setErrors(previousErrors => filterErrors(previousErrors, validationType)); - }; - - /** - * Validate a form field, running all its validations. - * If a validationType is provided then only that validation will be executed, - * skipping the other type of validation that might exist. - */ - const validate: Field['validate'] = (validationData = {}) => { - const { - formData = form.getFormData({ unflatten: false }), - value: valueToValidate = value, - validationType, - } = validationData; - - setValidating(true); - - // By the time our validate function has reached completion, it’s possible - // that validate() will have been called again. If this is the case, we need - // to ignore the results of this invocation and only use the results of - // the most recent invocation to update the error state for a field - const validateIteration = ++validateCounter.current; - - const onValidationResult = (validationErrors: ValidationError[]): FieldValidateResponse => { - if (validateIteration === validateCounter.current) { - // This is the most recent invocation - setValidating(false); - // Update the errors array - setErrors(previousErrors => { - // First filter out the validation type we are currently validating - const filteredErrors = filterErrors(previousErrors, validationType); - return [...filteredErrors, ...validationErrors]; - }); - } - return { - isValid: validationErrors.length === 0, - errors: validationErrors, - }; - }; - - if (isValidationAsync) { - return validateAsync({ - formData, - value: valueToValidate, - validationTypeToValidate: validationType, - }).then(onValidationResult); - } else { - const validationErrors = validateSync({ - formData, - value: valueToValidate, - validationTypeToValidate: validationType, - }); - return onValidationResult(validationErrors); - } - }; - - /** - * Handler to change the field value - * - * @param newValue The new value to assign to the field - */ - const setValue: Field['setValue'] = newValue => { - onValueChange(); - - const formattedValue = runFormatters(newValue); - setStateValue(formattedValue); - - // Update the form data observable - form.__updateFormDataAt(path, getOutputValue(formattedValue)); - }; - - const _setErrors: Field['setErrors'] = _errors => { - setErrors(_errors.map(error => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); - }; - - /** - * Form "onChange" event handler - * - * @param event Form input change event - */ - const onChange: Field['onChange'] = event => { - const newValue = {}.hasOwnProperty.call(event!.target, 'checked') - ? event.target.checked - : event.target.value; - - setValue(newValue); - }; - - /** - * As we can have multiple validation types (FIELD, ASYNC, ARRAY_ITEM), this - * method allows us to retrieve error messages for certain types of validation. - * - * For example, if we want to validation error messages to be displayed when the user clicks the "save" button - * _but_ in case of an asynchronous validation (for ex. an HTTP request that would validate an index name) we - * want to immediately display the error message, we would have 2 types of validation: FIELD & ASYNC - * - * @param validationType The validation type to return error messages from - */ - const getErrorsMessages: Field['getErrorsMessages'] = (args = {}) => { - const { errorCode, validationType = VALIDATION_TYPES.FIELD } = args; - const errorMessages = errors.reduce((messages, error) => { - const isSameErrorCode = errorCode && error.code === errorCode; - const isSamevalidationType = - error.validationType === validationType || - (validationType === VALIDATION_TYPES.FIELD && - !{}.hasOwnProperty.call(error, 'validationType')); - - if (isSameErrorCode || (typeof errorCode === 'undefined' && isSamevalidationType)) { - return messages ? `${messages}, ${error.message}` : (error.message as string); - } - return messages; - }, ''); - - return errorMessages ? errorMessages : null; - }; - - const getOutputValue: Field['__getOutputValue'] = (rawValue = value) => serializer(rawValue); - - // -- EFFECTS - // ---------------------------------- - useEffect(() => { - if (isPristine) { - // Avoid validate on mount - return; - } - form.__validateFields(fieldsToValidateOnChange); - }, [value]); - - const field: Field = { - path, - type, - label, - helpText, - value, - errors, - form, - isPristine, - isValidating, - isUpdating, - onChange, - getErrorsMessages, - setValue, - setErrors: _setErrors, - clearErrors, - validate, - __getOutputValue: getOutputValue, - }; - - form.__addField(field); - - return field; -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts deleted file mode 100644 index 04ac62a5fe8ec..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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 { useState, useRef } from 'react'; - -import { Form, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; -import { getAt, mapFormFields, flattenObject, unflattenObject, Subject } from '../lib'; - -const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; - -export const useForm = ( - formConfig: FormConfig | undefined = {} -): { form: Form } => { - const { - onSubmit, - schema, - defaultValue = {}, - serializer = (data: any) => data, - deSerializer = (data: any) => data, - options = { errorDisplayDelay: DEFAULT_ERROR_DISPLAY_TIMEOUT, stripEmptyFields: true }, - } = formConfig; - const defaultValueDeSerialized = - Object.keys(defaultValue).length === 0 ? defaultValue : deSerializer(defaultValue); - const [isSubmitted, setSubmitted] = useState(false); - const [isSubmitting, setSubmitting] = useState(false); - const [isValid, setIsValid] = useState(true); - const fieldsRefs = useRef({}); - - // formData$ is an observable we can subscribe to in order to receive live - // update of the raw form data. As an observable it does not trigger any React - // render(). - // The component is the one in charge of reading this observable - // and updating its state to trigger the necessary view render. - const formData$ = useRef>(new Subject(flattenObject(defaultValue) as T)); - - // -- HELPERS - // ---------------------------------- - const fieldsToArray = () => Object.values(fieldsRefs.current); - - const stripEmptyFields = (fields: FieldsMap): FieldsMap => { - if (options.stripEmptyFields) { - return Object.entries(fields).reduce( - (acc, [key, field]) => { - if (field.value !== '') { - acc[key] = field; - } - return acc; - }, - {} as FieldsMap - ); - } - return fields; - }; - - // -- API - // ---------------------------------- - const getFormData: Form['getFormData'] = (getDataOptions = { unflatten: true }) => - getDataOptions.unflatten - ? (unflattenObject( - mapFormFields(stripEmptyFields(fieldsRefs.current), field => field.__getOutputValue()) - ) as T) - : Object.entries(fieldsRefs.current).reduce( - (acc, [key, field]) => ({ - ...acc, - [key]: field.__getOutputValue(), - }), - {} as T - ); - - const updateFormDataAt: Form['__updateFormDataAt'] = (path, value) => { - const currentFormData = formData$.current.value; - formData$.current.next({ ...currentFormData, [path]: value }); - return formData$.current.value; - }; - - const validateFields: Form['__validateFields'] = async fieldNames => { - const fieldsToValidate = fieldNames - ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined) - : fieldsToArray().filter(field => field.isPristine); - - const formData = getFormData({ unflatten: false }); - - await Promise.all(fieldsToValidate.map(field => field.validate({ formData }))); - - const isFormValid = fieldsToArray().every( - field => field.getErrorsMessages() === null && !field.isValidating - ); - setIsValid(isFormValid); - - return isFormValid; - }; - - const addField: Form['__addField'] = field => { - fieldsRefs.current[field.path] = field; - - // Only update the formData if the path does not exist (it is the _first_ time - // the field is added), to avoid entering an infinite loop when the form is re-rendered. - if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) { - updateFormDataAt(field.path, field.__getOutputValue()); - } - }; - - const removeField: Form['__removeField'] = _fieldNames => { - const fieldNames = Array.isArray(_fieldNames) ? _fieldNames : [_fieldNames]; - const currentFormData = { ...formData$.current.value } as FormData; - - fieldNames.forEach(name => { - delete fieldsRefs.current[name]; - delete currentFormData[name]; - }); - - formData$.current.next(currentFormData as T); - }; - - const setFieldValue: Form['setFieldValue'] = (fieldName, value) => { - fieldsRefs.current[fieldName].setValue(value); - }; - - const setFieldErrors: Form['setFieldErrors'] = (fieldName, errors) => { - fieldsRefs.current[fieldName].setErrors(errors); - }; - - const getFields: Form['getFields'] = () => fieldsRefs.current; - - const getFieldDefaultValue: Form['getFieldDefaultValue'] = fieldName => - getAt(fieldName, defaultValueDeSerialized, false); - - const readFieldConfigFromSchema: Form['__readFieldConfigFromSchema'] = fieldName => { - const config = (getAt(fieldName, schema ? schema : {}, false) as FieldConfig) || {}; - - return config; - }; - - const onSubmitForm: Form['onSubmit'] = async e => { - if (e) { - e.preventDefault(); - } - - setSubmitting(true); - setSubmitted(true); // User has attempted to submit the form at least once - - const isFormValid = await validateFields(); - const formData = serializer(getFormData() as T); - - if (onSubmit) { - await onSubmit(formData, isFormValid); - } - - setSubmitting(false); - - return { data: formData, isValid: isFormValid }; - }; - - const form: Form = { - isSubmitted, - isSubmitting, - isValid, - options, - onSubmit: onSubmitForm, - setFieldValue, - setFieldErrors, - getFields, - getFormData, - getFieldDefaultValue, - __formData$: formData$, - __updateFormDataAt: updateFormDataAt, - __readFieldConfigFromSchema: readFieldConfigFromSchema, - __addField: addField, - __removeField: removeField, - __validateFields: validateFields, - }; - - return { - form, - }; -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts deleted file mode 100644 index 68c7cba15931a..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - */ - -// Only export the useForm hook. The "useField" hook is for internal use -// as the consumer of the library must use the component -export { useForm } from './hooks'; -export { fieldFormatters } from './lib'; - -export * from './components'; -export * from './constants'; -export * from './types'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts deleted file mode 100644 index 830c7725c95f4..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { Subject, Subscription } from './subject'; -export * from './utils'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts deleted file mode 100644 index 8ccc67a133355..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/subject.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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. - */ - -type Listener = (value: T) => void; - -export interface Subscription { - unsubscribe: () => void; -} - -export class Subject { - private callbacks: Set> = new Set(); - value: T; - - constructor(value: T) { - this.value = value; - } - - subscribe(fn: Listener): Subscription { - this.callbacks.add(fn); - - setTimeout(() => { - fn(this.value); - }); - - const unsubscribe = () => this.callbacks.delete(fn); - return { - unsubscribe, - }; - } - - next(value: T) { - this.value = value; - this.callbacks.forEach(fn => fn(value)); - } -} diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts deleted file mode 100644 index 7342b217abc62..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 { Field } from '../types'; - -const numRegEx = /^\d+$/; - -const isNumber = (val: string) => numRegEx.test(val); - -export const getAt = (path: string, object: any, throwIfNotFound = true): unknown => { - const pathToArray = path.split('.'); - const value = object[pathToArray[0]]; - - if (pathToArray.length === 1) { - return value; - } - - if (value !== null && typeof value === 'object') { - return getAt(pathToArray.slice(1).join('.'), value, throwIfNotFound); - } - - if (throwIfNotFound) { - throw new Error(`Can't access path "${path}" on ${JSON.stringify(object)}`); - } - - return undefined; -}; - -const setAt = (path: string, object: any, value: unknown, createUnknownPath = true): any => { - const pathToArray = path.split('.'); - - if (pathToArray.length === 1) { - object[pathToArray[0]] = value; - return object; - } - - let target = object; - - pathToArray.slice(0, -1).forEach((key, i) => { - if (!{}.hasOwnProperty.call(target, key)) { - if (createUnknownPath) { - // If the path segment is a number, we create an Array - // otherwise we create an object. - target[key] = isNumber(pathToArray[i + 1]) ? [] : {}; - } else { - throw new Error(`Can't set value "${value}" at "${path}" on ${JSON.stringify(object)}`); - } - } - - target = target[key]; - - if (target === null || (typeof target !== 'object' && !Array.isArray(target))) { - throw new Error( - `Can't set value "${value}" on a primitive. Path provided: "${path}", target: ${JSON.stringify( - object - )}` - ); - } - }); - - const keyToSet = pathToArray[pathToArray.length - 1]; - target[keyToSet] = value; - - return object; -}; - -export const unflattenObject = (object: any) => - Object.entries(object).reduce((acc, [key, field]) => { - setAt(key, acc, field); - return acc; - }, {}); - -export const flattenObject = ( - object: Record, - to: Record = {}, - paths: string[] = [] -): Record => - Object.entries(object).reduce((acc, [key, value]) => { - const updatedPaths = [...paths, key]; - if (value !== null && typeof value === 'object') { - return flattenObject(value, to, updatedPaths); - } - acc[updatedPaths.join('.')] = value; - return acc; - }, to); - -/** - * Helper to map the object of fields to any of its value - * - * @param formFields key value pair of path and form Fields - * @param fn Iterator function to execute on the field - */ -export const mapFormFields = (formFields: Record, fn: (field: Field) => any) => - Object.entries(formFields).reduce( - (acc, [key, field]) => { - acc[key] = fn(field); - return acc; - }, - {} as Record - ); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts deleted file mode 100644 index 2680b27ccca0b..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib/types.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 { ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react'; -import { Subject } from './lib'; - -export interface Form { - readonly isSubmitted: boolean; - readonly isSubmitting: boolean; - readonly isValid: boolean; - readonly options: FormOptions; - onSubmit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; - setFieldValue: (fieldName: string, value: FieldValue) => void; - setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; - getFields: () => FieldsMap; - getFormData: (options?: { unflatten?: boolean }) => T; - getFieldDefaultValue: (fieldName: string) => unknown; - readonly __formData$: MutableRefObject>; - __addField: (field: Field) => void; - __removeField: (fieldNames: string | string[]) => void; - __validateFields: (fieldNames?: string[]) => Promise; - __updateFormDataAt: (field: string, value: unknown) => T; - __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; -} - -export interface FormSchema { - [key: string]: FormSchemaEntry; -} -type FormSchemaEntry = - | FieldConfig - | Array> - | { [key: string]: FieldConfig | Array> | FormSchemaEntry }; - -export interface FormConfig { - onSubmit?: (data: T, isFormValid: boolean) => void; - schema?: FormSchema; - defaultValue?: Partial; - serializer?: SerializerFunc; - deSerializer?: SerializerFunc; - options?: FormOptions; -} - -export interface FormOptions { - errorDisplayDelay: number; - /** - * Remove empty string field ("") from form data - */ - stripEmptyFields: boolean; -} - -export interface Field { - readonly path: string; - readonly label?: string; - readonly helpText?: string; - readonly type: string; - readonly value: unknown; - readonly errors: ValidationError[]; - readonly isPristine: boolean; - readonly isValidating: boolean; - readonly isUpdating: boolean; - readonly form: Form; - getErrorsMessages: (args?: { - validationType?: 'field' | string; - errorCode?: string; - }) => string | null; - onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; - setValue: (value: FieldValue) => void; - setErrors: (errors: ValidationError[]) => void; - clearErrors: (type?: string | string[]) => void; - validate: (validateData?: { - formData?: any; - value?: unknown; - validationType?: string; - }) => FieldValidateResponse | Promise; - __getOutputValue: (rawValue?: unknown) => unknown; -} - -export interface FieldConfig { - readonly path?: string; - readonly label?: string; - readonly helpText?: string; - readonly type?: HTMLInputElement['type']; - readonly defaultValue?: unknown; - readonly validations?: Array>; - readonly formatters?: FormatterFunc[]; - readonly deSerializer?: SerializerFunc; - readonly serializer?: SerializerFunc; - readonly fieldsToValidateOnChange?: string[]; - readonly isValidationAsync?: boolean; - readonly errorDisplayDelay?: number; -} - -export interface FieldsMap { - [key: string]: Field; -} - -export type FormSubmitHandler = (formData: T, isValid: boolean) => Promise; - -export interface ValidationError { - message: string | ((error: ValidationError) => string); - code?: string; - validationType?: string; - [key: string]: any; -} - -export type ValidationFunc = (data: { - path: string; - value: unknown; - form: Form; - formData: T; - errors: readonly ValidationError[]; -}) => ValidationError | void | undefined | Promise; - -export interface FieldValidateResponse { - isValid: boolean; - errors: ValidationError[]; -} - -export type SerializerFunc = (value: any) => T; - -export type FormData = Record; - -type FormatterFunc = (value: any) => unknown; - -// We set it as unknown as a form field can be any of any type -// string | number | boolean | string[] ... -type FieldValue = unknown; - -export interface ValidationConfig { - validator: ValidationFunc; - type?: string; - exitOnFail?: boolean; -} diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts deleted file mode 100644 index a33b03853aeab..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/de_serializers.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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; - -// This deSerializer takes the previously selected options and map them -// against the default select options values. -export const multiSelectSelectedValueToOptions: FuncType = selectOptions => 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, - })); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts deleted file mode 100644 index 603a5d7e77777..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_formatters.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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); diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts deleted file mode 100644 index e6beb012d1e14..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/contains_char.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { containsChars } from '../../../validators/string'; -import { formatError } from '../../errors'; - -export const containsCharsField = ({ - message, - chars, -}: { - message: string; - chars: string | string[]; -}) => (...args: Parameters): ReturnType => { - const [{ value }] = args; - - const { doesContain, charsFound } = containsChars(chars)(value as string); - - if (doesContain) { - return { - ...formatError('CONTAINS_INVALID_CHARS', message), - charsFound, - }; - } -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts deleted file mode 100644 index fccf28c8e0b4d..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/empty_field.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 { fieldMissingError } from '../../errors'; - -export const emptyField = (message: string) => ( - ...args: Parameters -): ReturnType => { - const [{ value, path }] = args; - - if (typeof value === 'string') { - return isEmptyString(value) ? { ...fieldMissingError(path), message } : undefined; - } - - if (Array.isArray(value)) { - return isEmptyArray(value) ? { ...fieldMissingError(path), message } : undefined; - } -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts deleted file mode 100644 index ee47ae7b6cc07..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts deleted file mode 100644 index a70ec0d3036f0..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/index_name.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { formatError } from '../../errors'; - -const INDEX_ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', '*']; - -export const indexNameField = (...args: Parameters): ReturnType => { - const [{ value }] = args; - - if (startsWith('.')(value as string)) { - return formatError('INDEX_NAME', 'Cannot start with a dot (".").'); - } - - const { doesContain: doesContainSpaces } = containsChars(' ')(value as string); - if (doesContainSpaces) { - return formatError('INDEX_NAME', 'Cannot contain spaces.'); - } - - const { charsFound, doesContain } = containsChars(INDEX_ILLEGAL_CHARACTERS)(value as string); - if (doesContain) { - return formatError( - 'INDEX_NAME', - `Cannot contain the following characters: "${charsFound.join(',')}."` - ); - } -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts deleted file mode 100644 index 3f6aced2a81a7..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_length.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { hasMinLengthString } from '../../../validators/string'; -import { hasMinLengthArray } from '../../../validators/array'; -import { minLengthError } from '../../errors'; - -export const minLengthField = (length = 0) => ( - ...args: Parameters -): ReturnType => { - const [{ value }] = args; - - if (Array.isArray(value)) { - return hasMinLengthArray(length)(value) ? undefined : minLengthError(length); - } - return hasMinLengthString(length)((value as string).trim()) ? undefined : minLengthError(length); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts deleted file mode 100644 index 23267f49cc22b..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/min_selectable_selection.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 } from '../../hook_form_lib'; -import { hasMinLengthArray } from '../../../validators/array'; -import { minSelectionError } from '../../errors'; -import { multiSelectOptionsToSelectedValue } from '../../lib'; - -/** - * Validator to validate that a EuiSelectable has a minimum number - * of items selected. - * @param total Minimum number of items - */ -export const minSelectableSelectionField = (total = 0) => ( - ...args: Parameters -): ReturnType => { - const [{ value }] = args; - - return hasMinLengthArray(total)(multiSelectOptionsToSelectedValue(value as Option[])) - ? undefined - : minSelectionError(total); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts deleted file mode 100644 index 2fdb173a41e67..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators/url.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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, isUrl } from '../../../validators/string'; -import { formatError } from '../../errors'; - -export const urlField = (allowEmpty = false) => ( - ...args: Parameters -): ReturnType => { - const [{ value }] = args; - - if (typeof value !== 'string') { - return formatError('URL'); - } - - if (allowEmpty && isEmptyString(value)) { - return; - } - - return isUrl(value) ? undefined : formatError('URL'); -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts deleted file mode 100644 index 9af6b57ba3bef..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 './de_serializers'; -export * from './serializers'; -export * from './field_formatters'; diff --git a/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts b/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts deleted file mode 100644 index 3d4517d65923c..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/forms/lib/serializers.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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'; - -/** - * Return an array of labels of all the options that are selected - * - * @param value The Eui Selectable options array - */ -export const multiSelectOptionsToSelectedValue: SerializerFunc = ( - options: Option[] -): string[] => options.filter(option => option.checked === 'on').map(option => option.label); diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts deleted file mode 100644 index a03b307427b4d..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_max_length.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts deleted file mode 100644 index aaa8b810bed1a..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/array/has_min_length.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts deleted file mode 100644 index cb724e048c9e0..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/array/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts b/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts deleted file mode 100644 index f97caeb9d4e4c..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/array/is_empty.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts deleted file mode 100644 index 869a2477cfd4b..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/contains_chars.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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, - }; -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts deleted file mode 100644 index 58ba1ccfdc388..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/ends_with.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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); diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts deleted file mode 100644 index 371ccf36e8151..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_max_length.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts deleted file mode 100644 index bc12277c68284..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/has_min_length.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts deleted file mode 100644 index 4c34ebbb4886d..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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'; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts deleted file mode 100644 index b82eda817b451..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_empty.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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() === ''; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts deleted file mode 100644 index 5954760bf106a..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/is_url.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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; -}; diff --git a/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts b/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts deleted file mode 100644 index ffd840a07eebe..0000000000000 --- a/src/plugins/elasticsearch_ui_shared/static/validators/string/starts_with.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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); diff --git a/x-pack/legacy/plugins/index_management/public/app.js b/x-pack/legacy/plugins/index_management/public/app.js index 0b8c6be0e7929..07fa15a1a1332 100644 --- a/x-pack/legacy/plugins/index_management/public/app.js +++ b/x-pack/legacy/plugins/index_management/public/app.js @@ -12,7 +12,6 @@ import { TemplateCreate } from './sections/template_create'; import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { trackUiMetric } from './services'; -import { CreateIndex } from './sections/create_index'; export const App = () => { useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); @@ -27,7 +26,6 @@ export const App = () => { // Export this so we can test it with a different router. export const AppWithoutRouter = () => ( - Promise<{ isValid: boolean; data: Mappings }>; - -const initialData = { - dynamic: 'strict', - date_detection: false, - numeric_detection: true, - dynamic_date_formats: ['MM/dd/yyyy'], - properties: { - title: { - type: 'text', - store: true, - index: false, - fielddata: true, - }, - someObject: { - type: 'object', - store: true, - index: true, - fielddata: true, - properties: { - title: { - type: 'text', - store: true, - index: false, - fielddata: true, - }, - myDate: { - type: 'date', - store: true, - }, - superNested: { - type: 'object', - store: true, - index: true, - fielddata: true, - properties: { - lastName: { - type: 'text', - store: true, - index: false, - fielddata: true, - }, - name: { - type: 'text', - store: true, - index: true, - fielddata: true, - }, - }, - }, - }, - }, - someKeyword: { - type: 'text', - store: true, - index: false, - fielddata: true, - }, - }, -}; - -export const CreateIndex = () => { - const getMappingsEditorData = useRef(() => - Promise.resolve({ - isValid: true, - data: {}, - }) - ); - const [isFormSubmitted, setIsFormSubmitted] = useState(false); - const [mappings, setMappings] = useState(initialData); - const [mappingsState, setMappingsState] = useState>({ - isValid: true, - isEditingProperty: false, - }); - - const setGetMappingsEditorDataHandler = (handler: GetMappingsEditorDataHandler) => - (getMappingsEditorData.current = handler); - - const onClick = async () => { - const { isValid, data } = await getMappingsEditorData.current(); - // eslint-disable-next-line no-console - console.log(isValid, data); - setMappingsState(prev => ({ ...prev, isValid })); - setMappings(data); - setIsFormSubmitted(true); - }; - - return ( - - -

          Index Mappings...

          -
          - - - - - -
          - -

          - Everything below is OUTSIDE the MappingsEditor -

          - - {isFormSubmitted && mappingsState.isEditingProperty && ( - -

          - Warning: You are in the middle of editing a property. Please save the property before - continuing. -

          - -
          - )} - - Send form - - - - -

          Mappings editor data:

          -
          - - - {mappingsState.isValid ? ( -
          -          {JSON.stringify(mappings, null, 2)}
          -        
          - ) : ( -
          The mappings JSON data is not valid.
          - )} -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts b/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts deleted file mode 100644 index 2b556147894da..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/sections/create_index/index.ts +++ /dev/null @@ -1,7 +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 * from './create_index'; From f7168057345d96d7ff6bfb8c11bd9a64def78cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 29 Aug 2019 14:58:16 +0200 Subject: [PATCH 33/64] Refactor to use FormProvider component --- .../components/configuration_form.tsx | 33 +-- .../property_basic_parameters.tsx | 7 +- .../components/property/property_editor.tsx | 262 +++++++++--------- .../components/mappings_editor/form.schema.ts | 12 + 4 files changed, 164 insertions(+), 150 deletions(-) rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/{ => property}/property_basic_parameters.tsx (90%) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx index a7a54be9cfdaa..5b0371dcb83e9 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx @@ -9,6 +9,7 @@ import { EuiForm } from '@elastic/eui'; import { useForm, UseField, + FormProvider, } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { @@ -38,21 +39,21 @@ export const ConfigurationForm = ({ }, [form]); return ( - - {/* Global Mappings configuration */} - - - - - - - + + + + + + + + + + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx similarity index 90% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx index 5130feb42ab7b..d7ee0d50512a8 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx @@ -10,9 +10,9 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { UseField, Form, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; -import { parametersDefinition, ParameterName, DataTypeDefinition } from '../config'; +} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +import { parametersDefinition, ParameterName, DataTypeDefinition } from '../../config'; interface Props { form: Form; @@ -61,7 +61,6 @@ export const PropertyBasicParameters = ({ return ( ) => { return sanitizePropParameters(property); }; -const deSerializer = (property: Record) => { +const deserializer = (property: Record) => { if (!(dataTypesDefinition as any)[property.type]) { const type = getTypeFromSubType(property.type); if (!type) { @@ -111,7 +112,7 @@ export const PropertyEditor = ({ }: Props) => { const [isAdvancedSettingsVisible, setIsAdvancedSettingsVisible] = useState(false); - const { form } = useForm({ defaultValue, serializer, deSerializer }); + const { form } = useForm({ defaultValue, serializer, deserializer }); const isEditMode = typeof defaultValue !== 'undefined'; const submitForm = async () => { @@ -146,142 +147,143 @@ export const PropertyEditor = ({ }; return ( - - {formData => { - const selectedDatatype = formData.type as DataType; - const typeDefinition = dataTypesDefinition[selectedDatatype]; + + + {formData => { + const selectedDatatype = formData.type as DataType; + const typeDefinition = dataTypesDefinition[selectedDatatype]; - return ( - - - {/* Field name */} - - - - - {/* Field type */} - - - {field => ( - - { - setIsAdvancedSettingsVisible(false); - field.setValue(e.target.value); - }} - hasNoInitialSelection={true} - isInvalid={false} - options={Object.entries(dataTypesDefinition).map(([value, { label }]) => ({ - value, - text: label, - }))} - /> - - )} - - - - {/* Field sub type (if any) */} - {typeDefinition && typeDefinition.subTypes && ( + return ( + + + {/* Field name */} ({ - value: type, - text: type, - })), - hasNoInitialSelection: false, - }, + ...fieldConfig('name'), + validations: updateNameParameterValidations( + fieldConfig('name'), + parentObject, + form.getFieldDefaultValue('name') as string + ), }} + component={getComponentForParameter('name')} + componentProps={{ parentObject }} /> - )} - - {((typeDefinition && typeDefinition.basicParameters) || - getAdvancedSettingsCompForType(selectedDatatype)) && ( - - - - {typeDefinition && typeDefinition.basicParameters && ( - - {/* Basic parameters for the selected type */} - - - )} - {getAdvancedSettingsCompForType(selectedDatatype) && ( - - - {isAdvancedSettingsVisible ? 'Hide' : 'Show'} advanced settings - - - )} - - - )} + {/* Field type */} + + + {field => ( + + { + setIsAdvancedSettingsVisible(false); + field.setValue(e.target.value); + }} + hasNoInitialSelection={true} + isInvalid={false} + options={Object.entries(dataTypesDefinition).map( + ([value, { label }]) => ({ + value, + text: label, + }) + )} + /> + + )} + + + + {/* Field sub type (if any) */} + {typeDefinition && typeDefinition.subTypes && ( + + ({ + value: type, + text: type, + })), + hasNoInitialSelection: false, + }, + }} + /> + + )} + + + {((typeDefinition && typeDefinition.basicParameters) || + getAdvancedSettingsCompForType(selectedDatatype)) && ( + + + + {typeDefinition && typeDefinition.basicParameters && ( + + {/* Basic parameters for the selected type */} + + + )} + {getAdvancedSettingsCompForType(selectedDatatype) && ( + + + {isAdvancedSettingsVisible ? 'Hide' : 'Show'} advanced settings + + + )} + + + )} - {renderAdvancedSettings(selectedDatatype)} + {renderAdvancedSettings(selectedDatatype)} - - - - - Cancel - - - - - {isEditMode ? 'Done' : 'Add'} - - - - - ); - }} - + + + + + Cancel + + + + + {isEditMode ? 'Done' : 'Add'} + + + +
          + ); + }} + + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts index 7cbbe13fe0904..706ea1e0a9c85 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts @@ -7,8 +7,11 @@ import { FormSchema, FIELD_TYPES, + VALIDATION_TYPES, } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { containsCharsField } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; + export const schema: FormSchema = { dynamic: { label: 'Dynamic field', @@ -33,5 +36,14 @@ export const schema: FormSchema = { helpText: 'The dynamic_date_formats can be customised to support your own date formats.', type: FIELD_TYPES.COMBO_BOX, defaultValue: [], + validations: [ + { + validator: containsCharsField({ + message: 'Spaces are not allowed.', + chars: ' ', + }), + type: VALIDATION_TYPES.ARRAY_ITEM, + }, + ], }, }; From 88c0b179455e9ead3b84b7a9708f22362989980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 29 Aug 2019 15:06:55 +0200 Subject: [PATCH 34/64] Fix name parameter component --- .../mappings_editor/components/parameters/name.tsx | 2 +- .../mappings_editor/components/property/property_editor.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx index 2423520017347..ad33ae163a705 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -31,7 +31,7 @@ export const Name = ({ field, parentObject, fieldProps = {} }: Props) => { errorCode: ERROR_CODES.NAME_CONFLICT, }); - const isInvalid = field.errors.length ? !field.isUpdating : false; + const isInvalid = field.errors.length ? !field.isChangingValue : false; // Concatenate error messages. const errorMessage: string | null = diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx index e5674ff794157..dd2656692d2b5 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx @@ -46,19 +46,19 @@ interface Props { [key: string]: any; } -// We need to dynamically add the "nameValidation" as it validates +// We need to dynamically add the "uniqueNameValidation" as it validates // that the field name value provided does not already exist on the parent object. const updateNameParameterValidations = ( fieldConfig: FieldConfig, parentObject: Record, initialValue = '' ): ValidationConfig[] => { - const nameValidation: ValidationConfig['validator'] = ({ path, value, form, formData }) => { + const uniqueNameValidation: ValidationConfig['validator'] = ({ value }) => { if (Object.keys(parentObject).some(key => key !== initialValue && key === value)) { return nameConflictError(); } }; - return [...fieldConfig.validations!, { validator: nameValidation }]; + return [...fieldConfig.validations!, { validator: uniqueNameValidation }]; }; const fieldConfig = (param: ParameterName): FieldConfig => From ccda3f33aec88d998b420128e7f3569582f915de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 30 Aug 2019 09:56:34 +0200 Subject: [PATCH 35/64] Fix forward mappings editor data --- .../components/configuration_form.tsx | 12 ++-- .../components/document_fields.tsx | 8 +-- .../components/property/property_editor.tsx | 16 +++-- .../mappings_editor/mappings_editor.tsx | 67 +++++++++---------- 4 files changed, 47 insertions(+), 56 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx index 5b0371dcb83e9..04c59faa027b8 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx @@ -22,20 +22,16 @@ import { DYNAMIC_SETTING_OPTIONS } from '../constants'; interface Props { setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: any }>) => void; - onFormValidChange: (isValid: boolean) => void; + onValidityChange: (isValid: boolean) => void; defaultValue?: any; } -export const ConfigurationForm = ({ - setGetDataHandler, - defaultValue, - onFormValidChange, -}: Props) => { +export const ConfigurationForm = ({ setGetDataHandler, defaultValue, onValidityChange }: Props) => { const { form } = useForm({ schema, defaultValue }); useEffect(() => { - setGetDataHandler(form.onSubmit); - onFormValidChange(form.isValid); + setGetDataHandler(form.submit); + onValidityChange(form.isValid); }, [form]); return ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx index 44c0f856b6ebb..98f6386d6667b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx @@ -23,7 +23,7 @@ export const DocumentFields = ({ onUpdate }: Props) => { const { properties, selectedPath, selectedObjectToAddProperty } = usePropertiesState(); const dispatch = usePropertiesDispatch(); - const showCreateForm = selectedObjectToAddProperty === ''; + const isFormVisible = selectedObjectToAddProperty === ''; // an empty string means the root object "properties" useEffect(() => { onUpdate({ @@ -32,7 +32,7 @@ export const DocumentFields = ({ onUpdate }: Props) => { }); }, [properties, selectedPath, selectedObjectToAddProperty]); - const renderCreateForm = (style = {}) => ( + const renderForm = (style = {}) => ( {saveProperty => ( { ))} - {showCreateForm ? ( - renderCreateForm() + {isFormVisible ? ( + renderForm() ) : ( +const getFieldConfig = (param: ParameterName): FieldConfig => parametersDefinition[param].fieldConfig || {}; const defaultValueParam = (param: ParameterName): unknown => - typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; + typeof getFieldConfig(param).defaultValue !== 'undefined' + ? getFieldConfig(param).defaultValue + : ''; const sanitizePropParameters = (parameters: Record): Record => Object.entries(parameters).reduce( @@ -116,7 +118,7 @@ export const PropertyEditor = ({ const isEditMode = typeof defaultValue !== 'undefined'; const submitForm = async () => { - const { isValid, data: formData } = await form.onSubmit(); + const { isValid, data: formData } = await form.submit(); if (isValid) { const data = defaultValue && defaultValue.properties @@ -162,9 +164,9 @@ export const PropertyEditor = ({ path="name" defaultValue={isEditMode ? undefined : defaultValueParam('name')} // "undefined" means: look into the "defaultValue" object passed to the form config={{ - ...fieldConfig('name'), + ...getFieldConfig('name'), validations: updateNameParameterValidations( - fieldConfig('name'), + getFieldConfig('name'), parentObject, form.getFieldDefaultValue('name') as string ), @@ -178,7 +180,7 @@ export const PropertyEditor = ({ {field => ( @@ -215,7 +217,7 @@ export const PropertyEditor = ({ : typeDefinition.subTypes.types[0] } config={{ - ...fieldConfig('type'), + ...getFieldConfig('type'), label: typeDefinition.subTypes.label, }} component={Field} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 6ab4d2790eec5..eee7bc017e644 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import { ConfigurationForm, @@ -14,74 +14,67 @@ import { } from './components'; interface Props { - setGetDataHandler: ( - handler: () => Promise<{ isValid: boolean; data: Record }> - ) => void; - onStateUpdate: (state: State) => void; + setGetDataHandler: (handler: GetMappingsDataHandler) => void; + onStateUpdate: (state: Partial) => void; defaultValue?: Record; } +type GetMappingsDataHandler = () => Promise<{ isValid: boolean; mappings: Mappings }>; +type GetConfigFormDataHandler = () => Promise<{ isValid: boolean; data: Record }>; + export interface State { isValid: boolean; isEditingProperty: boolean; - properties: Record; + mappings: Mappings; } export type Mappings = Record; -type GetFormDataHandler = () => Promise<{ isValid: boolean; data: Record }>; +const DocumentFieldsMemo = React.memo(DocumentFields); +const ConfigurationFormMemo = React.memo(ConfigurationForm); export const MappingsEditor = ({ setGetDataHandler, onStateUpdate, defaultValue = {} }: Props) => { - const [state, setState] = useState({ - isValid: true, - isEditingProperty: false, - properties: {}, - }); - - const getConfigurationFormData = useRef(() => - Promise.resolve({ - isValid: true, - data: {}, - }) + const properties = useRef>({}); + const getConfigurationFormData = useRef(() => + Promise.resolve({ isValid: true, data: defaultValue }) ); useEffect(() => { setGetDataHandler(async () => { - const { isValid, data } = await getConfigurationFormData.current(); - return { - isValid, - data: { ...data, properties: state.properties }, - }; + const { isValid, data: configFormData } = await getConfigurationFormData.current(); + return { isValid, mappings: { ...configFormData, properties: properties.current } }; }); }, []); - useEffect(() => { - onStateUpdate(state); - }, [state]); + const setGetConfigurationFormDataHandler = useCallback((handler: GetConfigFormDataHandler) => { + getConfigurationFormData.current = handler; + }, []); - const setGetConfigurationFormDataHandler = (handler: GetFormDataHandler) => - (getConfigurationFormData.current = handler); + const onConfigFormValidityChange = useCallback( + (isValid: boolean) => onStateUpdate({ isValid }), + [] + ); + + const onDocumentFieldsUpdate = useCallback((docFieldsState: DocumentFieldsState) => { + properties.current = docFieldsState.properties; - const onDocumentFieldsUpdate = (docFieldsState: DocumentFieldsState) => { - setState(prev => ({ - ...prev, + onStateUpdate({ isEditingProperty: docFieldsState.isEditing, - properties: docFieldsState.properties, - })); - }; + }); + }, []); return (
          {/* Global Mappings configuration */} - setState(prev => ({ ...prev, isValid }))} + onValidityChange={onConfigFormValidityChange} defaultValue={defaultValue} /> {/* Document fields */} - +
          ); From 3c4c425bc5626972c75c7c90fd78c9a08e78fe03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 6 Sep 2019 10:58:51 +0200 Subject: [PATCH 36/64] Refactor to use the new
          context from the hook lib --- .../components/advanced_settings/keyword.tsx | 14 +--- .../components/advanced_settings/text.tsx | 23 ++---- .../components/configuration_form.tsx | 31 ++++--- .../components/document_fields.tsx | 4 +- .../components/parameters/name.tsx | 6 +- .../components/properties_contex.tsx | 6 +- .../property/property_basic_parameters.tsx | 7 +- .../components/property/property_editor.tsx | 27 +++--- .../mappings_editor/mappings_editor.tsx | 82 +++++++++---------- 9 files changed, 87 insertions(+), 113 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx index f841dea3eba97..b1ef9af3196bb 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx @@ -36,7 +36,6 @@ export const KeywordAdvancedSettings = ({ form, isEditMode }: Props) => { { { { { { { { { { { { { { { { { { { { { { { { - const { form } = useForm({ schema, defaultValue }); +export const ConfigurationForm = React.memo( + ({ setGetDataHandler, defaultValue, onValidityChange }: Props) => { + const { form } = useForm({ schema, defaultValue }); - useEffect(() => { - setGetDataHandler(form.submit); - onValidityChange(form.isValid); - }, [form]); + useEffect(() => { + setGetDataHandler(form.submit); + onValidityChange(form.isValid); + }, [form]); - return ( - - + return ( + @@ -49,7 +48,7 @@ export const ConfigurationForm = ({ setGetDataHandler, defaultValue, onValidityC - - - ); -}; + + ); + } +); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx index 98f6386d6667b..dd797f0c1140a 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx @@ -19,7 +19,7 @@ interface Props { onUpdate: (state: DocumentFieldsState) => void; } -export const DocumentFields = ({ onUpdate }: Props) => { +export const DocumentFields = React.memo(({ onUpdate }: Props) => { const { properties, selectedPath, selectedObjectToAddProperty } = usePropertiesState(); const dispatch = usePropertiesDispatch(); @@ -83,4 +83,4 @@ export const DocumentFields = ({ onUpdate }: Props) => { )} ); -}; +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx index ad33ae163a705..eff758ab58fb4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -14,10 +14,10 @@ import { ERROR_CODES } from '../../constants'; interface Props { field: FieldType; parentObject: Record; - fieldProps?: Record; + euiFieldProps?: Record; } -export const Name = ({ field, parentObject, fieldProps = {} }: Props) => { +export const Name = ({ field, parentObject, euiFieldProps = {} }: Props) => { const { errors } = field; // All validation messages, except name conflict @@ -55,7 +55,7 @@ export const Name = ({ field, parentObject, fieldProps = {} }: Props) => { onChange={field.onChange} isLoading={field.isValidating} fullWidth - {...fieldProps} + {...euiFieldProps} /> ); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx index 6218a27a4e14b..0158e5802b26e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx @@ -67,12 +67,12 @@ function propertiesReducer(state: State, action: Action): State { interface Props { children: React.ReactNode; - defaultProperties?: Record; + defaultValue?: Record; } -export const PropertiesProvider = ({ children, defaultProperties = {} }: Props) => { +export const PropertiesProvider = ({ children, defaultValue = {} }: Props) => { const [state, dispatch] = useReducer(propertiesReducer, { - properties: defaultProperties, + properties: defaultValue, selectedPath: null, selectedObjectToAddProperty: null, }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx index d7ee0d50512a8..c3288fa79757f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx @@ -7,15 +7,11 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - UseField, - Form, -} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { UseField } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; import { parametersDefinition, ParameterName, DataTypeDefinition } from '../../config'; interface Props { - form: Form; typeDefinition: DataTypeDefinition | null; isEditMode?: boolean; fieldPathPrefix?: string; @@ -34,7 +30,6 @@ const getMaxWidth = (rowIndex: number, totalItems: number) => { }; export const PropertyBasicParameters = ({ - form, typeDefinition, isEditMode = false, fieldPathPrefix = '', diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx index 577e310ad22c5..b9740880150a7 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx @@ -10,7 +10,6 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, - EuiForm, EuiFormRow, EuiSelect, } from '@elastic/eui'; @@ -18,7 +17,7 @@ import { import { useForm, UseField, - FormProvider, + Form, FormDataProvider, FieldConfig, ValidationConfig, @@ -114,12 +113,17 @@ export const PropertyEditor = ({ }: Props) => { const [isAdvancedSettingsVisible, setIsAdvancedSettingsVisible] = useState(false); - const { form } = useForm({ defaultValue, serializer, deserializer }); + const { form } = useForm({ + defaultValue, + serializer, + deserializer, + }); const isEditMode = typeof defaultValue !== 'undefined'; const submitForm = async () => { - const { isValid, data: formData } = await form.submit(); - if (isValid) { + const { isValid: isFormValid, data: formData } = await form.submit(); + + if (isFormValid) { const data = defaultValue && defaultValue.properties ? { ...formData, properties: defaultValue.properties } @@ -142,21 +146,21 @@ export const PropertyEditor = ({
          - +
          ); }; return ( - +
          {formData => { const selectedDatatype = formData.type as DataType; const typeDefinition = dataTypesDefinition[selectedDatatype]; return ( - + {/* Field name */} @@ -222,7 +226,7 @@ export const PropertyEditor = ({ }} component={Field} componentProps={{ - fieldProps: { + euiFieldProps: { options: typeDefinition.subTypes.types.map(type => ({ value: type, text: type, @@ -244,7 +248,6 @@ export const PropertyEditor = ({ {/* Basic parameters for the selected type */} @@ -282,10 +285,10 @@ export const PropertyEditor = ({ - + ); }} - +
          ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index eee7bc017e644..c1984158a8390 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -30,52 +30,52 @@ export interface State { export type Mappings = Record; -const DocumentFieldsMemo = React.memo(DocumentFields); -const ConfigurationFormMemo = React.memo(ConfigurationForm); +export const MappingsEditor = React.memo( + ({ setGetDataHandler, onStateUpdate, defaultValue = {} }: Props) => { + const properties = useRef>({}); + const getConfigurationFormData = useRef(() => + Promise.resolve({ isValid: true, data: defaultValue }) + ); -export const MappingsEditor = ({ setGetDataHandler, onStateUpdate, defaultValue = {} }: Props) => { - const properties = useRef>({}); - const getConfigurationFormData = useRef(() => - Promise.resolve({ isValid: true, data: defaultValue }) - ); + useEffect(() => { + setGetDataHandler(async () => { + const { isValid, data: configFormData } = await getConfigurationFormData.current(); - useEffect(() => { - setGetDataHandler(async () => { - const { isValid, data: configFormData } = await getConfigurationFormData.current(); - return { isValid, mappings: { ...configFormData, properties: properties.current } }; - }); - }, []); + return { isValid, mappings: { ...configFormData, properties: properties.current } }; + }); + }, []); - const setGetConfigurationFormDataHandler = useCallback((handler: GetConfigFormDataHandler) => { - getConfigurationFormData.current = handler; - }, []); + const setGetConfigurationFormDataHandler = useCallback((handler: GetConfigFormDataHandler) => { + getConfigurationFormData.current = handler; + }, []); - const onConfigFormValidityChange = useCallback( - (isValid: boolean) => onStateUpdate({ isValid }), - [] - ); + const onConfigFormValidityChange = useCallback( + (isValid: boolean) => onStateUpdate({ isValid }), + [] + ); - const onDocumentFieldsUpdate = useCallback((docFieldsState: DocumentFieldsState) => { - properties.current = docFieldsState.properties; + const onDocumentFieldsUpdate = useCallback((docFieldsState: DocumentFieldsState) => { + properties.current = docFieldsState.properties; - onStateUpdate({ - isEditingProperty: docFieldsState.isEditing, - }); - }, []); + onStateUpdate({ + isEditingProperty: docFieldsState.isEditing, + }); + }, []); - return ( -
          - {/* Global Mappings configuration */} - + return ( +
          + {/* Global Mappings configuration */} + - {/* Document fields */} - - - -
          - ); -}; + {/* Document fields */} + + + +
          + ); + } +); From 2747f18cb39b7ee3f32d40b8c9d66510d4df7610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 26 Sep 2019 17:28:05 +0200 Subject: [PATCH 37/64] Update import paths to form lib --- .../components/advanced_settings/keyword.tsx | 4 ++-- .../components/advanced_settings/text.tsx | 4 ++-- .../mappings_editor/components/configuration_form.tsx | 4 ++-- .../mappings_editor/components/parameters/index.ts | 4 ++-- .../mappings_editor/components/parameters/name.tsx | 2 +- .../components/property/property_basic_parameters.tsx | 4 ++-- .../components/property/property_editor.tsx | 4 ++-- .../mappings_editor/config/parameters_definition.ts | 11 ++++++----- .../static/ui/components/mappings_editor/errors.ts | 2 +- .../ui/components/mappings_editor/form.schema.ts | 6 ++++-- 10 files changed, 24 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx index b1ef9af3196bb..abcbde47a8d85 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx @@ -10,8 +10,8 @@ import { UseField, Form, FieldConfig, -} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; import { parametersDefinition, ParameterName } from '../../config'; import { INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx index a1fd9d474f7fd..96e91f5729275 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx @@ -10,8 +10,8 @@ import { UseField, Form, FieldConfig, -} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; import { parametersDefinition, ParameterName } from '../../config'; import { ANALYZERS_OPTIONS, INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx index c90ade3ce67c0..420e3d24eef15 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx @@ -9,12 +9,12 @@ import { useForm, UseField, Form, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { FormRow, Field, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; import { schema } from '../form.schema'; import { DYNAMIC_SETTING_OPTIONS } from '../constants'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts index dcdd744f307fd..757ebd64f2b70 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; -import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; +import { Field as FieldType } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { ParameterName } from '../../config'; import { Name } from './name'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx index eff758ab58fb4..b2be77b02452d 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { Field as FieldType } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { Field as FieldType } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { ERROR_CODES } from '../../constants'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx index c3288fa79757f..f623fa8b575e5 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx @@ -7,8 +7,8 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { UseField } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +import { UseField } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; import { parametersDefinition, ParameterName, DataTypeDefinition } from '../../config'; interface Props { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx index b9740880150a7..356c46b7edee2 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx @@ -21,8 +21,8 @@ import { FormDataProvider, FieldConfig, ValidationConfig, -} from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/components'; +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; import { nameConflictError } from '../../errors'; import { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts index 58fd857b020b4..683973c59a4ab 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts @@ -7,14 +7,15 @@ import { FieldConfig, FIELD_TYPES, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { - emptyField, - containsCharsField, -} from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; + fieldValidators, + fieldFormatters, +} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -import { toInt } from '../../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib'; +const { toInt } = fieldFormatters; +const { emptyField, containsCharsField } = fieldValidators; export type ParameterName = | 'name' diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts index d3a8e5bad1776..7304e63560633 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ValidationError } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +import { ValidationError } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { ERROR_CODES } from './constants'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts index 706ea1e0a9c85..2213af5af7ec2 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts @@ -8,9 +8,11 @@ import { FormSchema, FIELD_TYPES, VALIDATION_TYPES, -} from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/hook_form_lib'; +} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { containsCharsField } from '../../../../../../../../src/plugins/elasticsearch_ui_shared/static/forms/lib/field_validators'; +import { fieldValidators } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +const { containsCharsField } = fieldValidators; export const schema: FormSchema = { dynamic: { From a17b470da7cf6cd12344b589fe68a21fe922337a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 27 Sep 2019 11:11:57 +0200 Subject: [PATCH 38/64] Reset before refactor to v3 --- .../components/mappings_editor/_styles.scss | 76 ---- .../components/advanced_settings/index.ts | 19 - .../components/advanced_settings/keyword.tsx | 140 ------- .../components/advanced_settings/text.tsx | 204 --------- .../components/configuration_form.tsx | 54 --- .../components/document_fields.tsx | 86 ---- .../mappings_editor/components/index.ts | 12 - .../components/parameters/index.ts | 19 - .../components/parameters/name.tsx | 62 --- .../components/properties_contex.tsx | 103 ----- .../property/delete_property_provider.tsx | 92 ----- .../components/property/index.ts | 10 - .../property/property_basic_parameters.tsx | 74 ---- .../components/property/property_editor.tsx | 294 ------------- .../property/property_list_item.tsx | 146 ------- .../components/property/property_view.tsx | 50 --- .../property/save_property_provider.tsx | 176 -------- .../mappings_editor/components/tree/index.ts | 8 - .../mappings_editor/components/tree/tree.tsx | 67 --- .../components/tree/tree_item.tsx | 15 - .../config/data_types_definition.ts | 141 ------- .../mappings_editor/config/index.ts | 8 - .../config/parameters_definition.ts | 387 ------------------ .../components/mappings_editor/constants.ts | 40 -- .../ui/components/mappings_editor/errors.ts | 18 - .../components/mappings_editor/form.schema.ts | 51 --- .../ui/components/mappings_editor/helpers.ts | 74 ---- .../mappings_editor/mappings_editor.tsx | 73 ++-- 28 files changed, 38 insertions(+), 2461 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss deleted file mode 100644 index 6c9611d8da702..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/_styles.scss +++ /dev/null @@ -1,76 +0,0 @@ -.mappings-editor { - $gutter: 12px; - $border-tree: 1px dotted #aaa; - - .property-editor { - background-color: #fafbfd; - box-shadow: inset 0px 1px 3px 1px rgba(0,0,0,0.2); - margin-top: $gutter; - padding: $gutter; - position: relative; - padding-bottom: $gutter; - z-index: 1; - } - - .property-list-item__overlay { - position: absolute; - top: 0; - left: 1px; - right: 0; - height: 48px; - background-color: rgba(255, 255, 255, 0.65); - } - - .tree, - .tree ul { - margin:0; - padding:0; - list-style:none; - position:relative; - } - - .tree:before, - .tree ul:before { - content:""; - display:block; - width:0; - position:absolute; - top:0; - bottom:0; - left:0; - border-left: $border-tree; - } - - .tree li { - margin:0; - padding-left: $gutter * 2; - line-height:2em; - position:relative; - margin-bottom: $gutter; - - &:last-child { - .property-editor { - border-bottom: none; - } - } - } - - .tree li:before { - content:""; - display:block; - height:0; - border-top:1px dotted #aaa; - position:absolute; - top: 24px; - left: 1px; - width: $gutter; - } - - .tree li:last-child:before { - background:white; - height:auto; - top: $gutter * 2; - bottom:0; - left: 0; - } -} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts deleted file mode 100644 index f49b27c7c0b74..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/index.ts +++ /dev/null @@ -1,19 +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 { ComponentType } from 'react'; -import { DataType, SubType } from '../../config'; - -import { TextAdvancedSettings } from './text'; -import { KeywordAdvancedSettings } from './keyword'; - -const parameterMapToAdvancedSettingsComp: { [key in DataType | SubType]?: ComponentType } = { - text: TextAdvancedSettings, - keyword: KeywordAdvancedSettings, -}; - -export const getAdvancedSettingsCompForType = ( - type: DataType | SubType -): ComponentType | undefined => parameterMapToAdvancedSettingsComp[type]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx deleted file mode 100644 index abcbde47a8d85..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/keyword.tsx +++ /dev/null @@ -1,140 +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 React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { - UseField, - Form, - FieldConfig, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; - -import { parametersDefinition, ParameterName } from '../../config'; -import { INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; - -interface Props { - // fieldPathPrefix: string; - isEditMode: boolean; - form: Form; -} - -const fieldConfig = (param: ParameterName): FieldConfig => - parametersDefinition[param].fieldConfig || {}; - -const defaultValueParam = (param: ParameterName): unknown => - typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; - -export const KeywordAdvancedSettings = ({ form, isEditMode }: Props) => { - return ( - -
          - - - - - - - - - - - - - - -
          -
          - - - - - - - - -
          -
          - - - - - - - - - - - - - - -
          -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx deleted file mode 100644 index 96e91f5729275..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/advanced_settings/text.tsx +++ /dev/null @@ -1,204 +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 React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; - -import { - UseField, - Form, - FieldConfig, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; - -import { parametersDefinition, ParameterName } from '../../config'; -import { ANALYZERS_OPTIONS, INDEX_OPTIONS, SIMILARITY_ALGORITHM_OPTIONS } from '../../constants'; - -interface Props { - isEditMode: boolean; - form: Form; -} - -const fieldConfig = (param: ParameterName): FieldConfig => - parametersDefinition[param].fieldConfig || {}; - -const defaultValueParam = (param: ParameterName): unknown => - typeof fieldConfig(param).defaultValue !== 'undefined' ? fieldConfig(param).defaultValue : ''; - -export const TextAdvancedSettings = ({ form, isEditMode }: Props) => { - return ( - -
          - - - - - - - - - - - - - - -
          -
          - - - - - - - - - - - - - - -
          -
          - - - - - - - - - - - - - - - - - - - - - - - -
          -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx deleted file mode 100644 index 420e3d24eef15..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form.tsx +++ /dev/null @@ -1,54 +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 React, { useEffect } from 'react'; - -import { - useForm, - UseField, - Form, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; - -import { - FormRow, - Field, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; - -import { schema } from '../form.schema'; -import { DYNAMIC_SETTING_OPTIONS } from '../constants'; - -interface Props { - setGetDataHandler: (handler: () => Promise<{ isValid: boolean; data: any }>) => void; - onValidityChange: (isValid: boolean) => void; - defaultValue?: any; -} - -export const ConfigurationForm = React.memo( - ({ setGetDataHandler, defaultValue, onValidityChange }: Props) => { - const { form } = useForm({ schema, defaultValue }); - - useEffect(() => { - setGetDataHandler(form.submit); - onValidityChange(form.isValid); - }, [form]); - - return ( -
          - - - - - - -
          - ); - } -); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx deleted file mode 100644 index dd797f0c1140a..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields.tsx +++ /dev/null @@ -1,86 +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 React, { Fragment, useEffect } from 'react'; -import { EuiTitle, EuiSpacer, EuiButton } from '@elastic/eui'; - -import { Tree, TreeItem } from './tree'; -import { PropertyListItem, PropertyEditor, SavePropertyProvider } from './property'; -import { usePropertiesState, usePropertiesDispatch } from './properties_contex'; - -export interface DocumentFieldsState { - isEditing: boolean; - properties: Record; -} - -interface Props { - onUpdate: (state: DocumentFieldsState) => void; -} - -export const DocumentFields = React.memo(({ onUpdate }: Props) => { - const { properties, selectedPath, selectedObjectToAddProperty } = usePropertiesState(); - const dispatch = usePropertiesDispatch(); - - const isFormVisible = selectedObjectToAddProperty === ''; // an empty string means the root object "properties" - - useEffect(() => { - onUpdate({ - properties, - isEditing: selectedPath !== null || selectedObjectToAddProperty !== null, - }); - }, [properties, selectedPath, selectedObjectToAddProperty]); - - const renderForm = (style = {}) => ( - - {saveProperty => ( - ) => { - saveProperty({ newProperty, path: '', isEditMode: false, isCreateMode: true }); - }} - onCancel={() => dispatch({ type: 'selectObjectToAddProperty', value: null })} - parentObject={properties} - /> - )} - - ); - - return ( - - -

          Document fields

          -
          - - - - {Object.entries(properties) - // Make sure to display the fields in alphabetical order - .sort(([a], [b]) => (a < b ? -1 : 1)) - .map(([name, property], i) => ( - - - - ))} - - - {isFormVisible ? ( - renderForm() - ) : ( - dispatch!({ type: 'selectObjectToAddProperty', value: '' })} - isDisabled={selectedPath !== null || selectedObjectToAddProperty !== null} - > - Add property - - )} -
          - ); -}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts deleted file mode 100644 index dc065cb5d2f3a..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts +++ /dev/null @@ -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 * from './tree'; -export * from './property'; -export * from './configuration_form'; -export * from './document_fields'; -export * from './properties_contex'; -export { getComponentForParameter } from './parameters'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts deleted file mode 100644 index 757ebd64f2b70..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/index.ts +++ /dev/null @@ -1,19 +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 { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; -import { Field as FieldType } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { ParameterName } from '../../config'; -import { Name } from './name'; - -type FieldComponent = ({ field }: { field: FieldType }) => JSX.Element; - -const parameterMapToComponent: { [key in ParameterName]?: FieldComponent } = { - name: Name, -}; - -export const getComponentForParameter = (parameter: ParameterName): FieldComponent => - parameterMapToComponent[parameter] || Field; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx deleted file mode 100644 index b2be77b02452d..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/parameters/name.tsx +++ /dev/null @@ -1,62 +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 React from 'react'; -import { EuiFormRow, EuiFieldText } from '@elastic/eui'; - -import { Field as FieldType } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; - -import { ERROR_CODES } from '../../constants'; - -interface Props { - field: FieldType; - parentObject: Record; - euiFieldProps?: Record; -} - -export const Name = ({ field, parentObject, euiFieldProps = {} }: Props) => { - const { errors } = field; - - // All validation messages, except name conflict - const errorMessagesField = errors - .filter(err => err.code !== ERROR_CODES.NAME_CONFLICT) - .map(e => e.message as string) - .join(', '); - - // Name conflict error message - const errorMessagesNameConflict = field.getErrorsMessages({ - errorCode: ERROR_CODES.NAME_CONFLICT, - }); - - const isInvalid = field.errors.length ? !field.isChangingValue : false; - - // Concatenate error messages. - const errorMessage: string | null = - errorMessagesField && errorMessagesNameConflict - ? `${errorMessagesField}, ${errorMessagesNameConflict}` - : errorMessagesField - ? errorMessagesField - : errorMessagesNameConflict; - - return ( - - - - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx deleted file mode 100644 index 0158e5802b26e..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/properties_contex.tsx +++ /dev/null @@ -1,103 +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 React, { useReducer, useContext } from 'react'; -import { get, set } from 'lodash'; - -import { unset } from '../helpers'; - -interface State { - properties: Record; - selectedPath: string | null; - selectedObjectToAddProperty: string | null; -} - -type Action = - | { type: 'selectPath'; value: string | null } - | { type: 'selectObjectToAddProperty'; value: string | null } - | { type: 'saveProperty'; path: string; value: Record } - | { type: 'deleteProperty'; path: string } - | { type: 'updatePropertyPath'; oldPath: string; newPath: string }; - -type Dispatch = (action: Action) => void; - -const PropertiesStateContext = React.createContext(undefined); -const PropertiesDispatchContext = React.createContext(undefined); - -function propertiesReducer(state: State, action: Action): State { - let updatedProperties: Record; - - switch (action.type) { - case 'selectPath': - return { ...state, selectedPath: action.value }; - case 'selectObjectToAddProperty': - return { ...state, selectedObjectToAddProperty: action.value }; - case 'saveProperty': - updatedProperties = set({ ...state.properties }, action.path, action.value); - return { - ...state, - selectedPath: null, - selectedObjectToAddProperty: null, - properties: updatedProperties, - }; - case 'deleteProperty': - updatedProperties = { ...state.properties }; - unset(updatedProperties, action.path); - return { - ...state, - properties: updatedProperties, - }; - case 'updatePropertyPath': - const property = get(state.properties, action.oldPath); - // Delete the property at the old path - unset(state.properties, action.oldPath); - // Add it to the new path - updatedProperties = set({ ...state.properties }, action.newPath, property); - return { - ...state, - properties: updatedProperties, - }; - - default: - throw new Error(`Unhandled action type: ${action!.type}`); - } -} - -interface Props { - children: React.ReactNode; - defaultValue?: Record; -} - -export const PropertiesProvider = ({ children, defaultValue = {} }: Props) => { - const [state, dispatch] = useReducer(propertiesReducer, { - properties: defaultValue, - selectedPath: null, - selectedObjectToAddProperty: null, - }); - - return ( - - - {children} - - - ); -}; - -export const usePropertiesState = () => { - const context = useContext(PropertiesStateContext); - if (context === undefined) { - throw new Error('usePropertiesState must be used within a '); - } - return context; -}; - -export const usePropertiesDispatch = () => { - const context = useContext(PropertiesDispatchContext); - if (context === undefined) { - throw new Error('usePropertiesState must be used within a '); - } - return context; -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx deleted file mode 100644 index 2ec7fec689315..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/delete_property_provider.tsx +++ /dev/null @@ -1,92 +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 React, { useState, Fragment } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiTitle } from '@elastic/eui'; - -import { usePropertiesDispatch } from '../properties_contex'; -import { getNestedFieldMeta } from '../../helpers'; - -type DeletePropertyFunc = (property: Record, path: string) => void; - -interface Props { - children: (deleteProperty: DeletePropertyFunc) => React.ReactNode; -} - -interface State { - isModalOpen: boolean; - path: string | null; - property: Record | null; -} - -export const DeletePropertyProvider = ({ children }: Props) => { - const [state, setState] = useState({ isModalOpen: false, path: null, property: null }); - const dispatch = usePropertiesDispatch(); - - const closeModal = () => { - setState({ isModalOpen: false, path: null, property: null }); - }; - - const deleteProperty: DeletePropertyFunc = (property, path) => { - const { hasChildProperties } = getNestedFieldMeta(property); - - if (hasChildProperties) { - setState({ isModalOpen: true, property, path }); - } else { - dispatch({ type: 'deleteProperty', path }); - } - }; - - const confirmDelete = () => { - dispatch({ type: 'deleteProperty', path: state.path! }); - closeModal(); - }; - - const renderModal = () => { - const { property } = state; - const { nestedFieldPropName } = getNestedFieldMeta(property!); - const title = `Remove property '${property!.name}'?`; - const childrenCount = Object.keys(property![nestedFieldPropName!]).length; - - return ( - - - -

          - By deleting this property you will also delete its child{' '} - {childrenCount > 1 ? 'properties' : 'property'}, and all{' '} - {childrenCount > 1 ? 'their' : 'its'} possible nested properties. -

          - -

          Child properties that will also be deleted

          -
          -
            - {Object.keys(property![nestedFieldPropName!]) - .sort() - .map(name => ( -
          • {name}
          • - ))} -
          -
          -
          -
          - ); - }; - - return ( - - {children(deleteProperty)} - {state.isModalOpen && renderModal()} - - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts deleted file mode 100644 index d5941c1bef683..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/index.ts +++ /dev/null @@ -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 * from './property_list_item'; -export * from './property_editor'; -export * from './save_property_provider'; -export * from './delete_property_provider'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx deleted file mode 100644 index f623fa8b575e5..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_basic_parameters.tsx +++ /dev/null @@ -1,74 +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 React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { UseField } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; -import { parametersDefinition, ParameterName, DataTypeDefinition } from '../../config'; - -interface Props { - typeDefinition: DataTypeDefinition | null; - isEditMode?: boolean; - fieldPathPrefix?: string; -} - -const parametersToRows = (params: ParameterName[] | ParameterName[][]): ParameterName[][] => - Array.isArray(params[0]) ? (params as ParameterName[][]) : ([params] as ParameterName[][]); - -// If we have 2 or less items to display, we limit the width -// of the container to limit the size of the . -const getMaxWidth = (rowIndex: number, totalItems: number) => { - if (rowIndex === 0 || totalItems >= 3) { - return 'initial'; - } - return totalItems <= 1 ? '300px' : '600px'; -}; - -export const PropertyBasicParameters = ({ - typeDefinition, - isEditMode = false, - fieldPathPrefix = '', -}: Props) => { - if (!typeDefinition || !typeDefinition.basicParameters) { - return null; - } - - const rows = parametersToRows(typeDefinition.basicParameters); - - const defaultValueParam = (parameter: ParameterName): unknown | undefined => - isEditMode - ? undefined - : parametersDefinition[parameter] && - parametersDefinition[parameter].fieldConfig && - parametersDefinition[parameter].fieldConfig!.defaultValue; - - return ( - - {rows.map((parameters, i) => ( -
          - - {parameters.map(parameter => { - return ( - - - - ); - })} - -
          - ))} -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx deleted file mode 100644 index 356c46b7edee2..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_editor.tsx +++ /dev/null @@ -1,294 +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 React, { Fragment, useState } from 'react'; -import { - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiFormRow, - EuiSelect, -} from '@elastic/eui'; - -import { - useForm, - UseField, - Form, - FormDataProvider, - FieldConfig, - ValidationConfig, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { Field } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; - -import { nameConflictError } from '../../errors'; -import { - parametersDefinition, - dataTypesDefinition, - getTypeFromSubType, - ParameterName, - DataType, - SubType, -} from '../../config'; -import { PropertyBasicParameters } from './property_basic_parameters'; -import { getComponentForParameter } from '../parameters'; -import { getAdvancedSettingsCompForType } from '../advanced_settings'; - -interface Props { - onSubmit: (property: Record) => void; - onCancel: () => void; - defaultValue?: Record; - parentObject: Record; - [key: string]: any; -} - -// We need to dynamically add the "uniqueNameValidation" as it validates -// that the field name value provided does not already exist on the parent object. -const updateNameParameterValidations = ( - fieldConfig: FieldConfig, - parentObject: Record, - initialValue = '' -): ValidationConfig[] => { - const uniqueNameValidation: ValidationConfig['validator'] = ({ value }) => { - if (Object.keys(parentObject).some(key => key !== initialValue && key === value)) { - return nameConflictError(); - } - }; - return [...fieldConfig.validations!, { validator: uniqueNameValidation }]; -}; - -const getFieldConfig = (param: ParameterName): FieldConfig => - parametersDefinition[param].fieldConfig || {}; - -const defaultValueParam = (param: ParameterName): unknown => - typeof getFieldConfig(param).defaultValue !== 'undefined' - ? getFieldConfig(param).defaultValue - : ''; - -const sanitizePropParameters = (parameters: Record): Record => - Object.entries(parameters).reduce( - (acc, [param, value]) => { - // IF a prop value is "index_default", we remove it - if (value !== 'index_default') { - acc[param] = value; - } - return acc; - }, - {} as any - ); - -const serializer = (property: Record) => { - // If a subType is present, use it as type for ES - if ({}.hasOwnProperty.call(property, 'subType')) { - property.type = property.subType; - delete property.subType; - } - return sanitizePropParameters(property); -}; - -const deserializer = (property: Record) => { - if (!(dataTypesDefinition as any)[property.type]) { - const type = getTypeFromSubType(property.type); - if (!type) { - throw new Error( - `Property type "${property.type}" not recognized and no subType was found for it.` - ); - } - property.subType = property.type; - property.type = type; - } - - return property; -}; - -export const PropertyEditor = ({ - onSubmit, - onCancel, - defaultValue, - parentObject, - ...rest -}: Props) => { - const [isAdvancedSettingsVisible, setIsAdvancedSettingsVisible] = useState(false); - - const { form } = useForm({ - defaultValue, - serializer, - deserializer, - }); - const isEditMode = typeof defaultValue !== 'undefined'; - - const submitForm = async () => { - const { isValid: isFormValid, data: formData } = await form.submit(); - - if (isFormValid) { - const data = - defaultValue && defaultValue.properties - ? { ...formData, properties: defaultValue.properties } - : formData; - onSubmit(data); - } - }; - - const toggleAdvancedSettings = () => { - setIsAdvancedSettingsVisible(previous => !previous); - }; - - const renderAdvancedSettings = (type: DataType | SubType) => { - const AdvancedSettingsComponent = getAdvancedSettingsCompForType(type); - - if (!isAdvancedSettingsVisible || !AdvancedSettingsComponent) { - return null; - } - return ( - - -
          - -
          -
          - ); - }; - - return ( -
          - - {formData => { - const selectedDatatype = formData.type as DataType; - const typeDefinition = dataTypesDefinition[selectedDatatype]; - - return ( - - - {/* Field name */} - - - - - {/* Field type */} - - - {field => ( - - { - setIsAdvancedSettingsVisible(false); - field.setValue(e.target.value); - }} - hasNoInitialSelection={true} - isInvalid={false} - options={Object.entries(dataTypesDefinition).map( - ([value, { label }]) => ({ - value, - text: label, - }) - )} - /> - - )} - - - - {/* Field sub type (if any) */} - {typeDefinition && typeDefinition.subTypes && ( - - ({ - value: type, - text: type, - })), - hasNoInitialSelection: false, - }, - }} - /> - - )} - - - {((typeDefinition && typeDefinition.basicParameters) || - getAdvancedSettingsCompForType(selectedDatatype)) && ( - - - - {typeDefinition && typeDefinition.basicParameters && ( - - {/* Basic parameters for the selected type */} - - - )} - {getAdvancedSettingsCompForType(selectedDatatype) && ( - - - {isAdvancedSettingsVisible ? 'Hide' : 'Show'} advanced settings - - - )} - - - )} - - {renderAdvancedSettings(selectedDatatype)} - - - - - - Cancel - - - - - {isEditMode ? 'Done' : 'Add'} - - - - - ); - }} - -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx deleted file mode 100644 index 645e5eee44312..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_list_item.tsx +++ /dev/null @@ -1,146 +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 React, { Fragment, useState } from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; - -import { PropertyView } from './property_view'; -import { PropertyEditor } from './property_editor'; -import { SavePropertyProvider } from './save_property_provider'; -import { DeletePropertyProvider } from './delete_property_provider'; -import { Tree, TreeItem } from '../tree'; -import { usePropertiesState, usePropertiesDispatch } from '../properties_contex'; -import { getNestedFieldMeta, getParentObject } from '../../helpers'; -interface Props { - name: string; - path: string; - property: Record; - nestedDepth: number; -} - -export const PropertyListItem = ({ name, property, path, nestedDepth }: Props) => { - const { selectedPath, selectedObjectToAddProperty, properties } = usePropertiesState(); - const dispatch = usePropertiesDispatch(); - - const { - hasChildProperties, - nestedFieldPropName, - allowChildProperty, - childProperties, - } = getNestedFieldMeta(property); - - const isEditMode = selectedPath === path; - const isCreateMode = selectedObjectToAddProperty === path; - const isPropertyEditorVisible = isEditMode || isCreateMode; - const parentObject = getParentObject(path, properties); - const [showChildren, setShowChildren] = useState(isPropertyEditorVisible); - - const renderActionButtons = () => ( - - {allowChildProperty && ( - - dispatch({ type: 'selectObjectToAddProperty', value: path })} - iconType="plusInCircle" - aria-label="Add property" - disabled={selectedPath !== null || selectedObjectToAddProperty !== null} - /> - - )} - - dispatch({ type: 'selectPath', value: path })} - iconType="pencil" - aria-label="Edit property" - disabled={selectedPath !== null || selectedObjectToAddProperty !== null} - /> - - - - {deleteProperty => ( - deleteProperty({ name, ...property }, path)} - iconType="trash" - aria-label="Delete property" - disabled={selectedPath !== null || selectedObjectToAddProperty !== null} - /> - )} - - - - ); - - const renderEditForm = (style = {}) => ( - - {saveProperty => ( - ) => { - // Make sure the object is unfolded - setShowChildren(true); - saveProperty({ newProperty, oldProperty: property, path, isEditMode, isCreateMode }); - }} - onCancel={() => - isCreateMode - ? dispatch({ type: 'selectObjectToAddProperty', value: null }) - : dispatch({ type: 'selectPath', value: null }) - } - defaultValue={isCreateMode ? undefined : { name, ...property }} - parentObject={isCreateMode ? property[nestedFieldPropName!] : parentObject} - style={{ ...style, marginLeft: `${nestedDepth * -24 + 1}px` }} - /> - )} - - ); - - const renderPropertiesTree = () => ( - } - rightHeaderContent={renderActionButtons()} - isOpen={isPropertyEditorVisible ? true : showChildren} - onToggle={() => setShowChildren(prev => !prev)} - > - - {isPropertyEditorVisible && renderEditForm({ marginTop: 0, marginBottom: '12px' })} - {Object.entries(childProperties) - // Make sure to display the fields in alphabetical order - .sort(([a], [b]) => (a < b ? -1 : 1)) - .map(([childName, childProperty], i) => ( - - - - ))} - - - ); - - const renderNoChildren = () => ( - - - - - - {renderActionButtons()} - - {isPropertyEditorVisible && renderEditForm()} - - ); - - return allowChildProperty ? ( - - {isPropertyEditorVisible &&
          } - {hasChildProperties ? renderPropertiesTree() : renderNoChildren()} -
          - ) : ( - renderNoChildren() - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx deleted file mode 100644 index 22102ce260263..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/property_view.tsx +++ /dev/null @@ -1,50 +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 React, { Fragment } from 'react'; -import { EuiText, EuiTextColor, EuiFlexItem, EuiFlexGroup, EuiBadge } from '@elastic/eui'; - -interface Props { - name: string; - property: Record; -} - -export const PropertyView = ({ name, property }: Props) => { - const badges = ( - - {['index', 'doc_values', 'store', 'fielddata'] - .filter(field => Boolean(property[field])) - .map((field, i) => ( - - {field} - - ))} - - ); - - return ( - - {/* Name & Type */} - - - - {name} - - - - ({property.type}) - - - - - - {/* Badges */} - - {badges} - - - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx deleted file mode 100644 index c2398530ce914..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/property/save_property_provider.tsx +++ /dev/null @@ -1,176 +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 React, { useState, Fragment } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiTitle } from '@elastic/eui'; - -import { usePropertiesDispatch } from '../properties_contex'; -import { getNestedFieldMeta } from '../../helpers'; - -type SavePropertyFunc = (args: { - newProperty: Record; - oldProperty?: Record; - path: string; - isEditMode: boolean; - isCreateMode: boolean; -}) => void; - -interface Props { - children: (saveProperty: SavePropertyFunc) => React.ReactNode; -} - -interface State { - isModalOpen: boolean; - path: string | null; - newProperty: Record | null; - oldProperty: Record | null; -} - -export const SavePropertyProvider = ({ children }: Props) => { - const [state, setState] = useState({ - isModalOpen: false, - path: null, - newProperty: null, - oldProperty: null, - }); - const dispatch = usePropertiesDispatch(); - - const closeModal = () => { - setState({ isModalOpen: false, path: null, newProperty: null, oldProperty: null }); - }; - - const saveProperty: SavePropertyFunc = ({ - newProperty, - oldProperty, - path, - isCreateMode, - isEditMode, - }) => { - const { name: updatedName, ...rest } = newProperty; - - const handleUpdateFieldName = (newName: string): string => { - // The name has been updated, we need to - // 1. Change the property path to the new path - // 2. Replace the old property at the new path - const pathToArray = path.split('.'); - pathToArray[pathToArray.length - 1] = newName; - const newPath = pathToArray.join('.'); - - dispatch({ type: 'updatePropertyPath', oldPath: path, newPath }); - return newPath; - }; - - const handleUpdateFieldType = ( - oldType: string, - newType: string - ): { requiresConfirmation: boolean } => { - const { hasChildProperties } = getNestedFieldMeta(oldProperty!); - - if (!hasChildProperties) { - // No child properties will be deleted, no confirmation needed. - return { requiresConfirmation: false }; - } - - let requiresConfirmation = false; - - if (oldType === 'text' && newType !== 'keyword') { - requiresConfirmation = true; - } else if (oldType === 'keyword' && newType !== 'text') { - requiresConfirmation = true; - } else if (oldType === 'object' && newType !== 'nested') { - requiresConfirmation = true; - } else if (oldType === 'nested' && newType !== 'object') { - requiresConfirmation = true; - } - - return { requiresConfirmation }; - }; - - let pathToSaveProperty = path; - - if (isEditMode) { - if (updatedName !== name) { - pathToSaveProperty = handleUpdateFieldName(updatedName); - } - if (rest.type !== oldProperty!.type) { - // We need to check if, by changing the type, we need - // to delete the possible child properties ("fields" or "properties") - // and warn the user about it. - const { requiresConfirmation } = handleUpdateFieldType(oldProperty!.type, rest.type); - if (requiresConfirmation) { - setState({ isModalOpen: true, newProperty, oldProperty: oldProperty!, path }); - return; - } - } - } else if (isCreateMode) { - if (oldProperty) { - // If there is an "oldProperty" it means we want to add the property - // in either its "properties" or "fields" - // nestedFieldPropName is "properties" (for object and nested types) - // or "fields" (for text and keyword types). - const { nestedFieldPropName } = getNestedFieldMeta(oldProperty!); - pathToSaveProperty = `${path}.${nestedFieldPropName}.${updatedName}`; - } else { - // If there are no "oldProperty" we add the property to the top level - // "properties" object. - pathToSaveProperty = updatedName; - } - } - dispatch({ type: 'saveProperty', path: pathToSaveProperty, value: rest }); - }; - - const confirmTypeUpdate = () => { - delete state.newProperty!.fields; - delete state.newProperty!.properties; - dispatch({ type: 'saveProperty', path: state.path!, value: state.newProperty! }); - closeModal(); - }; - - const renderModal = () => { - const { newProperty, oldProperty } = state; - const title = `Confirm change '${newProperty!.name}' type to "${newProperty!.type}".`; - const { nestedFieldPropName } = getNestedFieldMeta(oldProperty!); - const childrenCount = Object.keys(oldProperty![nestedFieldPropName!]).length; - - return ( - - - -

          - By changing the type of this property you will also delete its child{' '} - {childrenCount > 1 ? 'properties' : 'property'}, and all{' '} - {childrenCount > 1 ? 'their' : 'its'} possible nested properties. -

          - -

          Nested {childrenCount > 1 ? 'properties' : 'property'} that will be removed

          -
          -
            - {Object.keys(oldProperty![nestedFieldPropName!]) - .sort() - .map(name => ( -
          • {name}
          • - ))} -
          -
          -
          -
          - ); - }; - - return ( - - {children(saveProperty)} - {state.isModalOpen && renderModal()} - - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts deleted file mode 100644 index c85cd21b1c5d0..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/index.ts +++ /dev/null @@ -1,8 +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 * from './tree'; -export * from './tree_item'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx deleted file mode 100644 index f5b4ea2e1b033..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree.tsx +++ /dev/null @@ -1,67 +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 React, { useState, Fragment } from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiIcon } from '@elastic/eui'; - -interface Props { - children: React.ReactNode; - defaultIsOpen?: boolean; - isOpen?: boolean; - onToggle?: () => void; - headerContent?: React.ReactNode; - rightHeaderContent?: React.ReactNode; -} - -export const Tree = ({ - children, - headerContent, - rightHeaderContent, - defaultIsOpen = false, - isOpen, - onToggle = () => undefined, -}: Props) => { - const hasHeader = Boolean(headerContent); - const isControlled = typeof isOpen !== 'undefined'; - const [showChildren, setShowChildren] = useState(defaultIsOpen); - const toggleShowChildren = () => setShowChildren(previous => !previous); - - const getIsOpen = () => (isControlled ? isOpen : showChildren); - const onMainBtnClick = () => (isControlled ? onToggle() : toggleShowChildren()); - - return ( - - {hasHeader && ( - - - - - {rightHeaderContent && {rightHeaderContent}} - - )} - {getIsOpen() && ( - - {hasHeader && } -
            {children}
          -
          - )} -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx deleted file mode 100644 index 72c78767229e7..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/tree/tree_item.tsx +++ /dev/null @@ -1,15 +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 React from 'react'; - -interface Props { - children: React.ReactNode; -} - -export const TreeItem = ({ children }: Props) => { - return
        • {children}
        • ; -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts deleted file mode 100644 index b8ef888c35b69..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/data_types_definition.ts +++ /dev/null @@ -1,141 +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 { ParameterName } from './parameters_definition'; - -export type DataType = - | 'text' - | 'keyword' - | 'numeric' - | 'date' - | 'binary' - | 'boolean' - | 'range' - | 'object' - | 'nested' - | 'ip' - | 'rank_feature' - | 'rank_features' - | 'dense_vector' - | 'sparse_vector'; - -export type SubType = NumericType | DateType | RangeType; - -export type NumericType = - | 'long' - | 'integer' - | 'short' - | 'byte' - | 'double' - | 'float' - | 'half_float' - | 'scaled_float'; - -export type DateType = 'date' | 'date_nanos'; - -export type RangeType = - | 'integer_range' - | 'float_range' - | 'long_range' - | 'double_range' - | 'date_range'; - -export interface DataTypeDefinition { - label: string; - subTypes?: { label: string; types: SubType[] }; - configuration?: ParameterName[]; - basicParameters?: ParameterName[] | ParameterName[][]; - hasAdvancedParameters?: boolean; - hasMultiFields?: boolean; -} - -export const dataTypesDefinition: { [key in DataType]: DataTypeDefinition } = { - text: { - label: 'Text', - basicParameters: ['store', 'index', 'fielddata'], - }, - keyword: { - label: 'Keyword', - basicParameters: ['store', 'index', 'doc_values'], - }, - numeric: { - label: 'Numeric', - subTypes: { - label: 'Numeric type', - types: ['long', 'integer', 'short', 'byte', 'double', 'float', 'half_float', 'scaled_float'], - }, - basicParameters: [ - ['store', 'index', 'coerce', 'doc_values', 'ignore_malformed'], - ['null_value', 'boost'], - ], - }, - date: { - label: 'Date', - subTypes: { - label: 'Date type', - types: ['date', 'date_nanos'], - }, - basicParameters: [ - ['store', 'index', 'doc_values', 'ignore_malformed'], - ['null_value', 'boost', 'locale', 'format'], - ], - }, - binary: { - label: 'Binary', - basicParameters: ['doc_values', 'store'], - }, - ip: { - label: 'IP', - basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], - }, - boolean: { - label: 'Boolean', - basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], - }, - range: { - label: 'Range', - subTypes: { - label: 'Range type', - types: ['integer_range', 'float_range', 'long_range', 'double_range', 'date_range'], - }, - basicParameters: [['store', 'index', 'coerce', 'doc_values'], ['boost']], - }, - object: { - label: 'Object', - basicParameters: ['dynamic', 'enabled'], - }, - nested: { - label: 'Nested', - basicParameters: ['dynamic'], - }, - rank_feature: { - label: 'Rank feature', - }, - rank_features: { - label: 'Rank features', - }, - dense_vector: { - label: 'Dense vector', - }, - sparse_vector: { - label: 'Sparse vector', - }, -}; - -const subTypesMapToType = Object.entries(dataTypesDefinition).reduce( - (acc, [type, definition]) => { - if ({}.hasOwnProperty.call(definition, 'subTypes')) { - definition.subTypes!.types.forEach(subType => { - acc[subType] = type; - }); - } - return acc; - }, - {} as Record -); - -export const getTypeFromSubType = (subType: SubType): DataType => - subTypesMapToType[subType] as DataType; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts deleted file mode 100644 index 35dab683cff24..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/index.ts +++ /dev/null @@ -1,8 +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 * from './data_types_definition'; -export * from './parameters_definition'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts deleted file mode 100644 index 683973c59a4ab..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/config/parameters_definition.ts +++ /dev/null @@ -1,387 +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 { - FieldConfig, - FIELD_TYPES, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; - -import { - fieldValidators, - fieldFormatters, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; - -const { toInt } = fieldFormatters; -const { emptyField, containsCharsField } = fieldValidators; - -export type ParameterName = - | 'name' - | 'type' - | 'store' - | 'index' - | 'fielddata' - | 'doc_values' - | 'coerce' - | 'ignore_malformed' - | 'null_value' - | 'dynamic' - | 'enabled' - | 'boost' - | 'locale' - | 'format' - | 'analyzer' - | 'search_analyzer' - | 'search_quote_analyzer' - | 'index_options' - | 'eager_global_ordinals' - | 'index_prefixes' - | 'index_phrases' - | 'norms' - | 'term_vector' - | 'position_increment_gap' - | 'similarity' - | 'normalizer' - | 'ignore_above' - | 'split_queries_on_whitespace'; - -export interface Parameter { - fieldConfig?: FieldConfig | Record; - paramName?: string; - docs?: string; -} - -export const parametersDefinition: { - [key in ParameterName]: Parameter; -} = { - name: { - fieldConfig: { - label: 'Field name', - defaultValue: '', - validations: [ - { - validator: emptyField('Give a name to the field.'), - }, - { - validator: containsCharsField({ - chars: ' ', - message: 'Spaces are not allowed in the name.', - }), - }, - ], - }, - }, - type: { - fieldConfig: { - label: 'Field type', - defaultValue: 'text', - type: FIELD_TYPES.SELECT, - }, - }, - store: { - fieldConfig: { - label: 'Store', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html', - }, - index: { - fieldConfig: { - label: 'Index', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - }, - doc_values: { - fieldConfig: { - label: 'Doc values', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', - }, - fielddata: { - fieldConfig: { - label: 'Fielddata', - type: FIELD_TYPES.CHECKBOX, - defaultValue: false, - }, - docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', - }, - coerce: { - fieldConfig: { - label: 'Coerce', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - }, - ignore_malformed: { - fieldConfig: { - label: 'Ignore malformed', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - }, - null_value: { - fieldConfig: { - label: 'Null value', - defaultValue: '', - type: FIELD_TYPES.TEXT, - }, - }, - boost: { - fieldConfig: { - label: 'Boost', - defaultValue: 1.0, - type: FIELD_TYPES.NUMBER, - formatters: [toInt], - validations: [ - { - validator: ({ value }) => { - if ((value as number) < 0) { - return { - message: 'The value must be greater or equal than 0.', - }; - } - }, - }, - ], - }, - }, - dynamic: { - fieldConfig: { - label: 'Dynamic', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - }, - enabled: { - fieldConfig: { - label: 'Enabled', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - }, - locale: { - fieldConfig: { - label: 'Locale', - defaultValue: '', - }, - }, - format: { - fieldConfig: { - label: 'Formats', - type: FIELD_TYPES.COMBO_BOX, - defaultValue: [], - serializer: (options: any[]): string | undefined => - options.length ? options.join('||') : undefined, - deSerializer: (formats?: string | any[]): any[] => - Array.isArray(formats) ? formats : (formats as string).split('||'), - }, - }, - analyzer: { - fieldConfig: { - label: 'Analyzer', - defaultValue: 'index_default', - type: FIELD_TYPES.SELECT, - }, - }, - search_analyzer: { - fieldConfig: { - label: 'Search analyzer', - defaultValue: 'index_default', - type: FIELD_TYPES.SELECT, - }, - }, - search_quote_analyzer: { - fieldConfig: { - label: 'Search quote analyzer', - defaultValue: 'index_default', - type: FIELD_TYPES.SELECT, - }, - }, - normalizer: { - fieldConfig: { - label: 'Normalizer', - defaultValue: '', - type: FIELD_TYPES.TEXT, - validations: [ - { - validator: containsCharsField({ - chars: ' ', - message: 'Spaces are not allowed.', - }), - }, - ], - }, - }, - index_options: { - fieldConfig: { - label: 'Index options', - defaultValue: 'docs', - type: FIELD_TYPES.SELECT, - }, - }, - eager_global_ordinals: { - fieldConfig: { - label: 'Eager global ordinals', - type: FIELD_TYPES.CHECKBOX, - defaultValue: false, - }, - }, - index_phrases: { - fieldConfig: { - label: 'Index phrases', - type: FIELD_TYPES.CHECKBOX, - defaultValue: false, - }, - }, - norms: { - fieldConfig: { - label: 'Norms', - type: FIELD_TYPES.CHECKBOX, - defaultValue: true, - }, - }, - term_vector: { - fieldConfig: { - label: 'Term vectors', - type: FIELD_TYPES.CHECKBOX, - defaultValue: false, - }, - }, - position_increment_gap: { - fieldConfig: { - label: 'Position increment gap', - type: FIELD_TYPES.NUMBER, - defaultValue: 100, - formatters: [toInt], - validations: [ - { - validator: emptyField('Set a position increment gap value.'), - }, - { - validator: ({ value }) => { - if ((value as number) < 0) { - return { - message: 'The value must be greater or equal than 0.', - }; - } - }, - }, - ], - }, - }, - index_prefixes: { - fieldConfig: { - min_chars: { - type: FIELD_TYPES.NUMBER, - defaultValue: 2, - helpText: 'Min chars.', - formatters: [toInt], - validations: [ - { - validator: emptyField('Set a min value.'), - }, - { - validator: ({ value }) => { - if ((value as number) < 0) { - return { - message: 'The value must be greater or equal than zero.', - }; - } - }, - }, - { - validator: ({ value, path, formData }) => { - const maxPath = path.replace('.min', '.max'); - const maxValue = formData[maxPath]; - - if ((maxValue as string) === '') { - return; - } - - if ((value as number) >= (maxValue as number)) { - return { - message: 'The value must be smaller than the max value.', - }; - } - }, - }, - ], - }, - max_chars: { - type: FIELD_TYPES.NUMBER, - defaultValue: 5, - helpText: 'Max chars.', - formatters: [toInt], - validations: [ - { - validator: emptyField('Set a max value.'), - }, - { - validator: ({ value }) => { - if ((value as number) > 20) { - return { - message: 'The value must be smaller or equal than 20.', - }; - } - }, - }, - { - validator: ({ value, path, formData }) => { - const minPath = path.replace('.max', '.min'); - const minValue = formData[minPath]; - - if ((minValue as string) === '') { - return; - } - - if ((value as number) <= (minValue as number)) { - return { - message: 'The value must be greater than the min value.', - }; - } - }, - }, - ], - }, - }, - }, - similarity: { - fieldConfig: { - label: 'Similarity algorithm', - defaultValue: 'BM25', - type: FIELD_TYPES.SELECT, - }, - }, - split_queries_on_whitespace: { - fieldConfig: { - label: 'Split queries on whitespace', - type: FIELD_TYPES.CHECKBOX, - defaultValue: false, - }, - }, - ignore_above: { - fieldConfig: { - label: 'Ignore above', - defaultValue: 2147483647, - type: FIELD_TYPES.NUMBER, - formatters: [toInt], - validations: [ - { - validator: ({ value }) => { - if ((value as number) < 0) { - return { - message: 'The value must be greater or equal than 0.', - }; - } - }, - }, - ], - }, - }, -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts deleted file mode 100644 index ecb0c0f10ce4c..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants.ts +++ /dev/null @@ -1,40 +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 const DYNAMIC_SETTING_OPTIONS = [ - { value: true, text: 'true' }, - { value: false, text: 'false' }, - { value: 'strict', text: 'strict' }, -]; - -export const ANALYZERS_OPTIONS = [ - { value: 'index_default', text: 'Index default' }, - { value: 'standard', text: 'Standard' }, - { value: 'simple', text: 'Simple' }, - { value: 'whitespace', text: 'Whitespace' }, - { value: 'stop', text: 'Stop' }, - { value: 'keyword', text: 'Keyword' }, - { value: 'pattern', text: 'Pattern' }, - { value: 'language', text: 'Language' }, - { value: 'fingerprint', text: 'Fingerprint' }, -]; - -export const INDEX_OPTIONS = [ - { value: 'docs', text: 'Docs' }, - { value: 'freqs', text: 'Freqs' }, - { value: 'positions', text: 'Positions' }, - { value: 'offsets', text: 'Offsets' }, -]; - -export const SIMILARITY_ALGORITHM_OPTIONS = [ - { value: 'BM25', text: 'BM25' }, - { value: 'classic', text: 'classic' }, - { value: 'boolean', text: 'boolean' }, -]; - -export const ERROR_CODES = { - NAME_CONFLICT: 'ERR_NAME_CONCLICT', -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts deleted file mode 100644 index 7304e63560633..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/errors.ts +++ /dev/null @@ -1,18 +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 { ValidationError } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; - -import { ERROR_CODES } from './constants'; - -/** - * Error creators - */ - -export const nameConflictError = (): ValidationError => ({ - code: ERROR_CODES.NAME_CONFLICT, - message: 'A field with the same name already exists.', -}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts deleted file mode 100644 index 2213af5af7ec2..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/form.schema.ts +++ /dev/null @@ -1,51 +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 { - FormSchema, - FIELD_TYPES, - VALIDATION_TYPES, -} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; - -import { fieldValidators } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; - -const { containsCharsField } = fieldValidators; - -export const schema: FormSchema = { - dynamic: { - label: 'Dynamic field', - helpText: 'Allow new fields discovery in document.', - type: FIELD_TYPES.SELECT, - defaultValue: true, - }, - date_detection: { - label: 'Date detection', - helpText: 'Check if the string field is a date.', - type: FIELD_TYPES.TOGGLE, - defaultValue: true, - }, - numeric_detection: { - label: 'Numeric field', - helpText: 'Check if the string field is a numeric value.', - type: FIELD_TYPES.TOGGLE, - defaultValue: true, - }, - dynamic_date_formats: { - label: 'Dynamic dates format', - helpText: 'The dynamic_date_formats can be customised to support your own date formats.', - type: FIELD_TYPES.COMBO_BOX, - defaultValue: [], - validations: [ - { - validator: containsCharsField({ - message: 'Spaces are not allowed.', - chars: ' ', - }), - type: VALIDATION_TYPES.ARRAY_ITEM, - }, - ], - }, -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts deleted file mode 100644 index afa429676f04f..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/helpers.ts +++ /dev/null @@ -1,74 +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 { get } from 'lodash'; -import { DataType } from './config'; - -export const hasNestedProperties = (selectedDatatype: DataType) => - selectedDatatype === 'object' || - selectedDatatype === 'nested' || - selectedDatatype === 'text' || - selectedDatatype === 'keyword'; - -const getNestedFieldsPropName = (selectedDatatype: DataType) => { - if (selectedDatatype === 'text' || selectedDatatype === 'keyword') { - return 'fields'; - } else if (selectedDatatype === 'object' || selectedDatatype === 'nested') { - return 'properties'; - } - return undefined; -}; - -export const getNestedFieldMeta = ( - property: Record -): { - hasChildProperties: boolean; - allowChildProperty: boolean; - nestedFieldPropName: 'fields' | 'properties' | undefined; - childProperties: Record; -} => { - const nestedFieldPropName = getNestedFieldsPropName(property.type); - const hasChildProperties = - typeof nestedFieldPropName !== 'undefined' && - Boolean(property[nestedFieldPropName]) && - Object.keys(property[nestedFieldPropName]).length > 0; - - const allowChildProperty = Boolean(nestedFieldPropName); - const childProperties = allowChildProperty && property[nestedFieldPropName!]; - return { hasChildProperties, nestedFieldPropName, allowChildProperty, childProperties }; -}; - -export const getParentObject = (path: string, object = {}): Record => { - const pathToArray = path.split('.'); - if (pathToArray.length === 1) { - return object; - } - const parentPath = pathToArray.slice(0, -1).join('.'); - return get(object, parentPath); -}; - -// We use an old version of lodash that does not have the _.unset() utility method. -// We implement our own here. -export const unset = (object: Record, path: string): boolean => { - const pathToArray = path.split('.'); - let hasBeenRemoved: boolean; - - if (pathToArray.length === 1) { - const [prop] = pathToArray; - hasBeenRemoved = {}.hasOwnProperty.call(object, prop); - delete object[prop]; - } else { - const parentObject = getParentObject(path, object); - if (!parentObject || typeof parentObject !== 'object') { - hasBeenRemoved = false; - } else { - const prop = pathToArray[pathToArray.length - 1]; - hasBeenRemoved = {}.hasOwnProperty.call(parentObject, prop); - delete (parentObject as any)[prop]; - } - } - - return hasBeenRemoved; -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index c1984158a8390..1c2334fc1ed16 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useRef, useCallback } from 'react'; +import React from 'react'; +// import React, { useEffect, useRef, useCallback } from 'react'; -import { - ConfigurationForm, - PropertiesProvider, - DocumentFields, - DocumentFieldsState, -} from './components'; +// import { +// ConfigurationForm, +// PropertiesProvider, +// DocumentFields, +// DocumentFieldsState, +// } from './components'; interface Props { setGetDataHandler: (handler: GetMappingsDataHandler) => void; @@ -20,7 +21,7 @@ interface Props { } type GetMappingsDataHandler = () => Promise<{ isValid: boolean; mappings: Mappings }>; -type GetConfigFormDataHandler = () => Promise<{ isValid: boolean; data: Record }>; +// type GetConfigFormDataHandler = () => Promise<{ isValid: boolean; data: Record }>; export interface State { isValid: boolean; @@ -32,49 +33,51 @@ export type Mappings = Record; export const MappingsEditor = React.memo( ({ setGetDataHandler, onStateUpdate, defaultValue = {} }: Props) => { - const properties = useRef>({}); - const getConfigurationFormData = useRef(() => - Promise.resolve({ isValid: true, data: defaultValue }) - ); + // const properties = useRef>({}); + // const getConfigurationFormData = useRef(() => + // Promise.resolve({ isValid: true, data: defaultValue }) + // ); - useEffect(() => { - setGetDataHandler(async () => { - const { isValid, data: configFormData } = await getConfigurationFormData.current(); + // useEffect(() => { + // setGetDataHandler(async () => { + // const { isValid, data: configFormData } = await getConfigurationFormData.current(); - return { isValid, mappings: { ...configFormData, properties: properties.current } }; - }); - }, []); + // return { isValid, mappings: { ...configFormData, properties: properties.current } }; + // }); + // }, []); - const setGetConfigurationFormDataHandler = useCallback((handler: GetConfigFormDataHandler) => { - getConfigurationFormData.current = handler; - }, []); + // const setGetConfigurationFormDataHandler = useCallback((handler: GetConfigFormDataHandler) => { + // getConfigurationFormData.current = handler; + // }, []); - const onConfigFormValidityChange = useCallback( - (isValid: boolean) => onStateUpdate({ isValid }), - [] - ); + // const onConfigFormValidityChange = useCallback( + // (isValid: boolean) => onStateUpdate({ isValid }), + // [] + // ); - const onDocumentFieldsUpdate = useCallback((docFieldsState: DocumentFieldsState) => { - properties.current = docFieldsState.properties; + // const onDocumentFieldsUpdate = useCallback((docFieldsState: DocumentFieldsState) => { + // properties.current = docFieldsState.properties; - onStateUpdate({ - isEditingProperty: docFieldsState.isEditing, - }); - }, []); + // onStateUpdate({ + // isEditingProperty: docFieldsState.isEditing, + // }); + // }, []); return (
          {/* Global Mappings configuration */} - Global configuration + {/* + /> */} {/* Document fields */} - +

          Document Fields

          + {/* - +
          */}
          ); } From d885bfea183f6b6068bd5a5f4fb83bad1546d0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 30 Sep 2019 10:43:02 +0200 Subject: [PATCH 39/64] Add subscribe method to form lib + update validation logic I added a "subscribe()" method to the form hook in order to listen for data changes. The handler receives the raw data + a data getter handler to format the data. It also returns a "validate()" method to be able to validate the form from anywhere. I split the logic to validate fields vs validate *all* fields in 2 function to make the logic clearer. --- .../forms/hook_form_lib/hooks/use_field.ts | 11 ++- .../forms/hook_form_lib/hooks/use_form.ts | 97 ++++++++++++++----- .../static/forms/hook_form_lib/lib/utils.ts | 2 +- .../static/forms/hook_form_lib/types.ts | 20 +++- 4 files changed, 95 insertions(+), 35 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 8a1012404b377..d55ef5d19c39c 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -43,6 +43,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); const [isChangingValue, setIsChangingValue] = useState(false); + const [isValidated, setIsValidated] = useState(false); const validateCounter = useRef(0); const changeCounter = useRef(0); const inflightValidation = useRef | null>(null); @@ -262,6 +263,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) validationType, } = validationData; + setIsValidated(true); setValidating(true); // By the time our validate function has reached completion, it’s possible @@ -275,12 +277,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) // This is the most recent invocation setValidating(false); // Update the errors array - setErrors(previousErrors => { - // First filter out the validation type we are currently validating - const filteredErrors = filterErrors(previousErrors, validationType); - return [...filteredErrors, ..._validationErrors]; - }); + const filteredErrors = filterErrors(errors, validationType); + setErrors([...filteredErrors, ..._validationErrors]); } + return { isValid: _validationErrors.length === 0, errors: _validationErrors, @@ -388,6 +388,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) form, isPristine, isValidating, + isValidated, isChangingValue, onChange, getErrorsMessages, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 360182368ae63..9b048849069db 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -17,11 +17,11 @@ * under the License. */ -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { get } from 'lodash'; -import { FormHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; -import { mapFormFields, flattenObject, unflattenObject, Subject } from '../lib'; +import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; +import { mapFormFields, flattenObject, unflattenObject, Subject, Subscription } from '../lib'; const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; const DEFAULT_OPTIONS = { @@ -49,8 +49,9 @@ export function useForm( Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue); const [isSubmitted, setSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); - const [isValid, setIsValid] = useState(true); + const [isValid, setIsValid] = useState(undefined); const fieldsRefs = useRef({}); + const formUpdateSubscribers = useRef([]); // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React @@ -59,6 +60,13 @@ export function useForm( // and updating its state to trigger the necessary view render. const formData$ = useRef>(new Subject(flattenObject(defaultValue) as T)); + useEffect(() => { + return () => { + formUpdateSubscribers.current.forEach(subscription => subscription.unsubscribe()); + formUpdateSubscribers.current = []; + }; + }, []); + // -- HELPERS // ---------------------------------- const fieldsToArray = () => Object.values(fieldsRefs.current); @@ -78,6 +86,12 @@ export function useForm( return fields; }; + const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => { + const currentFormData = formData$.current.value; + formData$.current.next({ ...currentFormData, [path]: value }); + return formData$.current.value; + }; + // -- API // ---------------------------------- const getFormData: FormHook['getFormData'] = (getDataOptions = { unflatten: true }) => @@ -93,34 +107,52 @@ export function useForm( {} as T ); - const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => { - const currentFormData = formData$.current.value; - formData$.current.next({ ...currentFormData, [path]: value }); - return formData$.current.value; + const isFieldValid = (field: FieldHook) => + field.getErrorsMessages() === null && !field.isValidating; + + const updateFormValidity = () => { + const fieldsArray = fieldsToArray(); + const areAllFieldsValidated = fieldsArray.every(field => field.isValidated); + + if (!areAllFieldsValidated) { + // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" + return; + } + + const isFormValid = fieldsArray.every(isFieldValid); + + setIsValid(isFormValid); + return isFormValid; }; - /** - * When a field value changes, validateFields() is called with the field name + any other fields - * declared in the "fieldsToValidateOnChange" (see the field config). - * - * When this method is called _without_ providing any fieldNames, we only need to validate fields that are pristine - * as the fields that are dirty have already been validated when their value changed. - */ const validateFields: FormHook['__validateFields'] = async fieldNames => { const fieldsToValidate = fieldNames - ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined) - : fieldsToArray().filter(field => field.isPristine); // only validate fields that haven't been changed + .map(name => fieldsRefs.current[name]) + .filter(field => field !== undefined); - const formData = getFormData({ unflatten: false }); + if (fieldsToValidate.length === 0) { + // Nothing to validate + return true; + } + const formData = getFormData({ unflatten: false }); await Promise.all(fieldsToValidate.map(field => field.validate({ formData }))); + updateFormValidity(); - const isFormValid = fieldsToArray().every( - field => field.getErrorsMessages() === null && !field.isValidating - ); - setIsValid(isFormValid); + return fieldsToValidate.every(isFieldValid); + }; - return isFormValid; + const validateAllFields = async (): Promise => { + const fieldsToValidate = fieldsToArray().filter(field => !field.isValidated); + + if (fieldsToValidate.length === 0) { + // Nothing left to validate, all fields are already validated. + return isValid!; + } + + await validateFields(fieldsToValidate.map(field => field.path)); + + return updateFormValidity()!; }; const addField: FormHook['__addField'] = field => { @@ -174,16 +206,28 @@ export function useForm( } setSubmitting(true); - const isFormValid = await validateFields(); + const isFormValid = await validateAllFields(); const formData = serializer(getFormData() as T); if (onSubmit) { - await onSubmit(formData, isFormValid); + await onSubmit(formData, isFormValid!); } setSubmitting(false); - return { data: formData, isValid: isFormValid }; + return { data: formData, isValid: isFormValid! }; + }; + + const subscribe: FormHook['subscribe'] = handler => { + const format = () => serializer(getFormData() as T); + const validate = async () => await validateAllFields(); + + const subscription = formData$.current.subscribe(raw => { + handler({ isValid, data: { raw, format }, validate }); + }); + + formUpdateSubscribers.current.push(subscription); + return subscription; }; const form: FormHook = { @@ -191,6 +235,7 @@ export function useForm( isSubmitting, isValid, submit: submitForm, + subscribe, setFieldValue, setFieldErrors, getFields, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 66c3e8d983f98..06e2a69a8afd8 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -33,7 +33,7 @@ export const flattenObject = ( ): Record => Object.entries(object).reduce((acc, [key, value]) => { const updatedPaths = [...paths, key]; - if (value !== null && typeof value === 'object') { + if (value !== null && !Array.isArray(value) && typeof value === 'object') { return flattenObject(value, to, updatedPaths); } acc[updatedPaths.join('.')] = value; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 28e2a346bd5c4..3449951877702 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -18,7 +18,7 @@ */ import { ReactNode, ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react'; -import { Subject } from './lib'; +import { Subject, Subscription } from './lib'; // This type will convert all optional property to required ones // Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623 @@ -27,8 +27,9 @@ type Required = T extends object ? { [P in keyof T]-?: NonNullable } : export interface FormHook { readonly isSubmitted: boolean; readonly isSubmitting: boolean; - readonly isValid: boolean; + readonly isValid: boolean | undefined; submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + subscribe: (handler: OnUpdateHandler) => Subscription; setFieldValue: (fieldName: string, value: FieldValue) => void; setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; getFields: () => FieldsMap; @@ -38,7 +39,7 @@ export interface FormHook { readonly __formData$: MutableRefObject>; __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; - __validateFields: (fieldNames?: string[]) => Promise; + __validateFields: (fieldNames: string[]) => Promise; __updateFormDataAt: (field: string, value: unknown) => T; __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; } @@ -46,6 +47,7 @@ export interface FormHook { export interface FormSchema { [key: string]: FormSchemaEntry; } + type FormSchemaEntry = | FieldConfig | Array> @@ -60,6 +62,17 @@ export interface FormConfig { options?: FormOptions; } +export interface OnFormUpdateArg { + data: { + raw: { [key: string]: any }; + format: () => T; + }; + validate: () => Promise; + isValid?: boolean; +} + +export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; + export interface FormOptions { errorDisplayDelay?: number; /** @@ -77,6 +90,7 @@ export interface FieldHook { readonly errors: ValidationError[]; readonly isPristine: boolean; readonly isValidating: boolean; + readonly isValidated: boolean; readonly isChangingValue: boolean; readonly form: FormHook; getErrorsMessages: (args?: { From ee8419ff5f71df1813df1a4c6f31d205cd40e1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 30 Sep 2019 10:47:06 +0200 Subject: [PATCH 40/64] Refactor logic to update configuration form and send to consumer --- .../configuration_form/configuration_form.tsx | 51 +++++++ .../configuration_form/form.schema.ts | 55 +++++++ .../components/configuration_form/index.ts | 7 + .../mappings_editor/components/index.ts | 7 + .../field_configuration/field_options.ts | 36 +++++ .../field_configuration/index.ts | 7 + .../mappings_editor/mappings_editor.tsx | 136 +++++++++--------- .../mappings_editor/shared_imports.ts | 24 ++++ 8 files changed, 255 insertions(+), 68 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx new file mode 100644 index 0000000000000..f8dd22fbe7a23 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -0,0 +1,51 @@ +/* + * 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, { useEffect } from 'react'; + +import { useForm, UseField, Form, OnFormUpdateArg } from '../../shared_imports'; +import { FormRow, Field } from '../../shared_imports'; +import { DYNAMIC_SETTING_OPTIONS } from '../../field_configuration'; +import { schema } from './form.schema'; + +export type ConfigurationUpdateHandler = (arg: OnFormUpdateArg) => void; + +export interface MappingsConfiguration { + dynamic: boolean | string; + date_detection: boolean; + numeric_detection: boolean; + dynamic_date_formats: string[]; +} + +interface Props { + onUpdate: ConfigurationUpdateHandler; + defaultValue?: any; +} + +export const ConfigurationForm = React.memo(({ onUpdate, defaultValue }: Props) => { + const { form } = useForm({ schema, defaultValue }); + + useEffect(() => { + const subscription = form.subscribe(onUpdate); + return subscription.unsubscribe; + }, [form, onUpdate]); + + return ( +
          + + + + + + +
          + ); +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts new file mode 100644 index 0000000000000..689df08980928 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts @@ -0,0 +1,55 @@ +/* + * 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 { FormSchema, FIELD_TYPES, VALIDATION_TYPES, fieldValidators } from '../../shared_imports'; +import { MappingsConfiguration } from './configuration_form'; + +const { containsCharsField } = fieldValidators; + +export const schema: FormSchema = { + dynamic: { + label: 'Dynamic field', + helpText: 'Allow new fields discovery in document.', + type: FIELD_TYPES.SELECT, + defaultValue: true, + }, + date_detection: { + label: 'Date detection', + helpText: 'Check if the string field is a date.', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + numeric_detection: { + label: 'Numeric field', + helpText: 'Check if the string field is a numeric value.', + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, + dynamic_date_formats: { + label: 'Dynamic dates format', + helpText: 'The dynamic_date_formats can be customised to support your own date formats.', + type: FIELD_TYPES.COMBO_BOX, + defaultValue: [], + validations: [ + { + validator: ({ value }) => { + if ((value as string[]).length === 0) { + return { + message: 'Add at least one', + }; + } + }, + }, + { + validator: containsCharsField({ + message: 'Spaces are not allowed.', + chars: ' ', + }), + type: VALIDATION_TYPES.ARRAY_ITEM, + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts new file mode 100644 index 0000000000000..0829143b71488 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './configuration_form'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts new file mode 100644 index 0000000000000..0829143b71488 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './configuration_form'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts new file mode 100644 index 0000000000000..b5005f5f390d7 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts @@ -0,0 +1,36 @@ +/* + * 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 const DYNAMIC_SETTING_OPTIONS = [ + { value: true, text: 'true' }, + { value: false, text: 'false' }, + { value: 'strict', text: 'strict' }, +]; + +// export const ANALYZERS_OPTIONS = [ +// { value: 'index_default', text: 'Index default' }, +// { value: 'standard', text: 'Standard' }, +// { value: 'simple', text: 'Simple' }, +// { value: 'whitespace', text: 'Whitespace' }, +// { value: 'stop', text: 'Stop' }, +// { value: 'keyword', text: 'Keyword' }, +// { value: 'pattern', text: 'Pattern' }, +// { value: 'language', text: 'Language' }, +// { value: 'fingerprint', text: 'Fingerprint' }, +// ]; + +// export const INDEX_OPTIONS = [ +// { value: 'docs', text: 'Docs' }, +// { value: 'freqs', text: 'Freqs' }, +// { value: 'positions', text: 'Positions' }, +// { value: 'offsets', text: 'Offsets' }, +// ]; + +// export const SIMILARITY_ALGORITHM_OPTIONS = [ +// { value: 'BM25', text: 'BM25' }, +// { value: 'classic', text: 'classic' }, +// { value: 'boolean', text: 'boolean' }, +// ]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts new file mode 100644 index 0000000000000..78dab4b754908 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './field_options'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 1c2334fc1ed16..d60eb3edfccac 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,81 +4,81 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -// import React, { useEffect, useRef, useCallback } from 'react'; +import React, { useRef, useCallback } from 'react'; -// import { -// ConfigurationForm, -// PropertiesProvider, -// DocumentFields, -// DocumentFieldsState, -// } from './components'; +import { OnFormUpdateArg } from './shared_imports'; +import { + ConfigurationForm, + ConfigurationUpdateHandler, + MappingsConfiguration, + // PropertiesProvider, + // DocumentFields, + // DocumentFieldsState, +} from './components'; + +export interface Mappings extends MappingsConfiguration { + properties: any; +} + +export interface OnUpdateHandlerArg { + isValid?: boolean; + getData: DataGetterHandler; + validate: ValidityGetterHandler; +} + +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + +export type DataGetterHandler = () => Mappings; +export type ValidityGetterHandler = () => Promise; interface Props { - setGetDataHandler: (handler: GetMappingsDataHandler) => void; - onStateUpdate: (state: Partial) => void; + onUpdate: OnUpdateHandler; defaultValue?: Record; } -type GetMappingsDataHandler = () => Promise<{ isValid: boolean; mappings: Mappings }>; -// type GetConfigFormDataHandler = () => Promise<{ isValid: boolean; data: Record }>; +export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props) => { + const configurationForm = useRef | undefined>(undefined); -export interface State { - isValid: boolean; - isEditingProperty: boolean; - mappings: Mappings; -} + const onMappingsDataUpdate = async () => { + if (configurationForm.current === undefined) { + return; + } + + const isMappingsEditorValid = configurationForm.current.isValid; // for now we only check configurationForm + + const getData = (): Mappings => { + const configurationData = configurationForm.current!.data.format(); + const properties = {}; + + return { ...configurationData, properties }; + }; + + const validate = async () => { + const isConfigurationFormValid = await configurationForm.current!.validate(); + + return isConfigurationFormValid; + }; + + onUpdate({ isValid: isMappingsEditorValid, getData, validate }); + }; + + const onConfigurationFormUpdate = useCallback(data => { + configurationForm.current = data; + + onMappingsDataUpdate(); + }, []); + + return ( +
          + {/* Global Mappings configuration */} +

          Global configuration

          + -export type Mappings = Record; - -export const MappingsEditor = React.memo( - ({ setGetDataHandler, onStateUpdate, defaultValue = {} }: Props) => { - // const properties = useRef>({}); - // const getConfigurationFormData = useRef(() => - // Promise.resolve({ isValid: true, data: defaultValue }) - // ); - - // useEffect(() => { - // setGetDataHandler(async () => { - // const { isValid, data: configFormData } = await getConfigurationFormData.current(); - - // return { isValid, mappings: { ...configFormData, properties: properties.current } }; - // }); - // }, []); - - // const setGetConfigurationFormDataHandler = useCallback((handler: GetConfigFormDataHandler) => { - // getConfigurationFormData.current = handler; - // }, []); - - // const onConfigFormValidityChange = useCallback( - // (isValid: boolean) => onStateUpdate({ isValid }), - // [] - // ); - - // const onDocumentFieldsUpdate = useCallback((docFieldsState: DocumentFieldsState) => { - // properties.current = docFieldsState.properties; - - // onStateUpdate({ - // isEditingProperty: docFieldsState.isEditing, - // }); - // }, []); - - return ( -
          - {/* Global Mappings configuration */} -

          Global configuration

          - {/* */} - - {/* Document fields */} -

          Document Fields

          - {/* + {/* Document fields */} +

          Document Fields

          + {/* */} -
          - ); - } -); +
          + ); +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts new file mode 100644 index 0000000000000..c6a4d0c3bcee6 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts @@ -0,0 +1,24 @@ +/* + * 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 { + useForm, + UseField, + Form, + FormSchema, + FIELD_TYPES, + VALIDATION_TYPES, + OnFormUpdateArg, +} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + FormRow, + Field, +} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { + fieldValidators, +} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; From 3186bf20bdd655f3a460525cce85ce1e8b97fe86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 30 Sep 2019 13:10:07 +0200 Subject: [PATCH 41/64] Use reducer to manage editor state --- .../configuration_form/configuration_form.tsx | 31 ++--- .../configuration_form/form.schema.ts | 2 +- .../components/configuration_form/index.ts | 4 + .../mappings_editor/mappings_editor.tsx | 83 +++-------- .../mappings_editor/mappings_state.tsx | 131 ++++++++++++++++++ .../mappings_editor/shared_imports.ts | 1 + 6 files changed, 173 insertions(+), 79 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx index f8dd22fbe7a23..3ffb1b0e46987 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -5,32 +5,30 @@ */ import React, { useEffect } from 'react'; -import { useForm, UseField, Form, OnFormUpdateArg } from '../../shared_imports'; +import { useForm, getUseField, Form, OnFormUpdateArg } from '../../shared_imports'; import { FormRow, Field } from '../../shared_imports'; import { DYNAMIC_SETTING_OPTIONS } from '../../field_configuration'; +import { MappingsConfiguration, useDispatch } from '../../mappings_state'; import { schema } from './form.schema'; export type ConfigurationUpdateHandler = (arg: OnFormUpdateArg) => void; -export interface MappingsConfiguration { - dynamic: boolean | string; - date_detection: boolean; - numeric_detection: boolean; - dynamic_date_formats: string[]; -} - interface Props { - onUpdate: ConfigurationUpdateHandler; - defaultValue?: any; + defaultValue?: MappingsConfiguration; } -export const ConfigurationForm = React.memo(({ onUpdate, defaultValue }: Props) => { +const UseField = getUseField({ component: Field }); + +export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { const { form } = useForm({ schema, defaultValue }); + const dispatch = useDispatch(); useEffect(() => { - const subscription = form.subscribe(onUpdate); + const subscription = form.subscribe(updatedConfiguration => { + dispatch({ type: 'updateConfiguration', value: updatedConfiguration }); + }); return subscription.unsubscribe; - }, [form, onUpdate]); + }, [form]); return (
          @@ -40,11 +38,10 @@ export const ConfigurationForm = React.memo(({ onUpdate, defaultValue }: Props) componentProps={{ euiFieldProps: { options: DYNAMIC_SETTING_OPTIONS }, }} - component={Field} /> - - - + + + ); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts index 689df08980928..6c6d7af651414 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts @@ -5,7 +5,7 @@ */ import { FormSchema, FIELD_TYPES, VALIDATION_TYPES, fieldValidators } from '../../shared_imports'; -import { MappingsConfiguration } from './configuration_form'; +import { MappingsConfiguration } from '../../mappings_state'; const { containsCharsField } = fieldValidators; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts index 0829143b71488..333e032a69193 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from './form.schema'; + export * from './configuration_form'; + +export const CONFIGURATION_FIELDS = Object.keys(schema); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index d60eb3edfccac..762dbb0b12ec0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -4,81 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef, useCallback } from 'react'; +import React from 'react'; -import { OnFormUpdateArg } from './shared_imports'; +// import { OnFormUpdateArg } from './shared_imports'; import { ConfigurationForm, - ConfigurationUpdateHandler, - MappingsConfiguration, - // PropertiesProvider, + CONFIGURATION_FIELDS, // DocumentFields, // DocumentFieldsState, } from './components'; -export interface Mappings extends MappingsConfiguration { - properties: any; -} - -export interface OnUpdateHandlerArg { - isValid?: boolean; - getData: DataGetterHandler; - validate: ValidityGetterHandler; -} - -export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; - -export type DataGetterHandler = () => Mappings; -export type ValidityGetterHandler = () => Promise; +import { + MappingsState, + Props as MappingsStateProps, + MappingsConfiguration, +} from './mappings_state'; interface Props { - onUpdate: OnUpdateHandler; - defaultValue?: Record; + onUpdate: MappingsStateProps['onUpdate']; + defaultValue?: { [key: string]: any }; } export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props) => { - const configurationForm = useRef | undefined>(undefined); - - const onMappingsDataUpdate = async () => { - if (configurationForm.current === undefined) { - return; - } - - const isMappingsEditorValid = configurationForm.current.isValid; // for now we only check configurationForm - - const getData = (): Mappings => { - const configurationData = configurationForm.current!.data.format(); - const properties = {}; - - return { ...configurationData, properties }; - }; - - const validate = async () => { - const isConfigurationFormValid = await configurationForm.current!.validate(); - - return isConfigurationFormValid; - }; - - onUpdate({ isValid: isMappingsEditorValid, getData, validate }); - }; - - const onConfigurationFormUpdate = useCallback(data => { - configurationForm.current = data; - - onMappingsDataUpdate(); - }, []); + const configurationDefaultValue = Object.entries(defaultValue) + .filter(([key]) => CONFIGURATION_FIELDS.includes(key)) + .reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value, + }), + {} as MappingsConfiguration + ); return ( -
          - {/* Global Mappings configuration */} -

          Global configuration

          - - - {/* Document fields */} + +

          Document Fields

          - {/* - - */} -
          + ); }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx new file mode 100644 index 0000000000000..ce70f672de29e --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -0,0 +1,131 @@ +/* + * 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, { useReducer, useEffect, createContext, useContext } from 'react'; + +import { OnFormUpdateArg } from './shared_imports'; + +export interface MappingsConfiguration { + dynamic: boolean | string; + date_detection: boolean; + numeric_detection: boolean; + dynamic_date_formats: string[]; +} + +export interface MappingsProperties { + [key: string]: any; +} + +export type Mappings = MappingsConfiguration & { + properties: MappingsProperties; +}; + +export interface OnUpdateHandlerArg { + isValid?: boolean; + getData: () => Mappings; + validate: () => Promise; +} + +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + +interface State { + isValid: boolean | undefined; + configuration: OnFormUpdateArg; + properties: { + data: { [key: string]: any }; + status: 'idle' | 'editing' | 'creating'; + isValid: boolean | undefined; + }; +} + +type Action = + | { type: 'updateConfiguration'; value: OnFormUpdateArg } + | { type: 'addProperty'; value: any }; + +type Dispatch = (action: Action) => void; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'updateConfiguration': + const isValid = + action.value.isValid === undefined || state.properties.isValid === undefined + ? undefined + : action.value.isValid && state.properties.isValid; + + return { + ...state, + isValid, + configuration: action.value, + }; + case 'addProperty': + return state; + default: + throw new Error(`Action "${action!.type}" not recognized.`); + } +}; + +const StateContext = createContext(undefined); +const DispatchContext = createContext(undefined); + +const initialState: State = { + isValid: undefined, + configuration: { + data: { + raw: {}, + format: () => ({} as Mappings), + }, + validate: () => Promise.resolve(false), + }, + properties: { + data: {}, + status: 'idle', + isValid: true, + }, +}; + +export interface Props { + children: React.ReactNode; + onUpdate: OnUpdateHandler; +} + +export const MappingsState = ({ children, onUpdate }: Props) => { + const [state, dispatch] = useReducer(reducer, initialState); + useEffect(() => { + // console.log('State update', state); + onUpdate({ + getData: () => ({ + ...state.configuration.data.format(), + properties: state.properties.data, + }), + validate: () => { + return state.configuration.validate(); + }, + isValid: state.isValid, + }); + }, [state]); + + return ( + + {children} + + ); +}; + +export const useState = () => { + const ctx = useContext(StateContext); + if (ctx === undefined) { + throw new Error('useState must be used within a '); + } + return ctx; +}; + +export const useDispatch = () => { + const ctx = useContext(DispatchContext); + if (ctx === undefined) { + throw new Error('useDispatch must be used within a '); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts index c6a4d0c3bcee6..38c04c75662a0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts @@ -7,6 +7,7 @@ export { useForm, UseField, + getUseField, Form, FormSchema, FIELD_TYPES, From 5a8ce4bfb182be9177a46d7396f456bf9335e05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 30 Sep 2019 13:43:46 +0200 Subject: [PATCH 42/64] Add DocumentFields component with Properties --- .../document_fields/document_fields.tsx | 23 ++++++++++++ .../document_fields_header.tsx | 10 +++++ .../components/document_fields/index.ts | 7 ++++ .../components/document_fields/properties.tsx | 26 +++++++++++++ .../components/document_fields/property.tsx | 25 +++++++++++++ .../mappings_editor/components/index.ts | 2 + .../ui/components/mappings_editor/index.ts | 2 + .../mappings_editor/mappings_editor.tsx | 13 ++----- .../mappings_editor/mappings_state.tsx | 37 ++++++++++--------- 9 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx new file mode 100644 index 0000000000000..7d7c07787d40a --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -0,0 +1,23 @@ +/* + * 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 { DocumentFieldsHeaders } from './document_fields_header'; +import { Properties, Props as PropertiesProps } from './properties'; + +interface Props { + defaultValue?: PropertiesProps['defaultValue']; +} + +export const DocumentFields = ({ defaultValue }: Props) => { + return ( + <> + + + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx new file mode 100644 index 0000000000000..7ebde00f26502 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx @@ -0,0 +1,10 @@ +/* + * 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'; + +export const DocumentFieldsHeaders = () => { + return

          Document fields

          ; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/index.ts new file mode 100644 index 0000000000000..be58e7c599ac0 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './document_fields'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx new file mode 100644 index 0000000000000..63c3a22f39c9b --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx @@ -0,0 +1,26 @@ +/* + * 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 { Property, IProperty } from './property'; + +export interface Props { + defaultValue?: { [key: string]: Omit }; +} + +export const Properties = ({ defaultValue = {} }: Props) => { + const propertiesArray = Object.entries(defaultValue).map(([name, value]) => ({ name, ...value })); + + return ( +
            + {propertiesArray.map(prop => ( +
          • + +
          • + ))} +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx new file mode 100644 index 0000000000000..b39224d973dbb --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx @@ -0,0 +1,25 @@ +/* + * 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'; + +export interface IProperty { + name: string; + type: string; +} + +interface Props { + value: IProperty; +} + +export const Property = ({ value }: Props) => { + return ( +
          +

          + {value.name} | {value.type} +

          +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts index 0829143b71488..4110db5a39312 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/index.ts @@ -5,3 +5,5 @@ */ export * from './configuration_form'; + +export * from './document_fields'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts index 47c0a71408a7c..4c178b2a82fd1 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts @@ -5,3 +5,5 @@ */ export * from './mappings_editor'; + +export { OnUpdateHandler, Mappings } from './mappings_state'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 762dbb0b12ec0..c6cef8e21cd76 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -6,13 +6,7 @@ import React from 'react'; -// import { OnFormUpdateArg } from './shared_imports'; -import { - ConfigurationForm, - CONFIGURATION_FIELDS, - // DocumentFields, - // DocumentFieldsState, -} from './components'; +import { ConfigurationForm, CONFIGURATION_FIELDS, DocumentFields } from './components'; import { MappingsState, @@ -35,11 +29,12 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props }), {} as MappingsConfiguration ); + const propertiesDefaultValue = defaultValue.properties || {}; return ( - + -

          Document Fields

          +
          ); }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index ce70f672de29e..8c76394dc2387 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -70,28 +70,29 @@ const reducer = (state: State, action: Action): State => { const StateContext = createContext(undefined); const DispatchContext = createContext(undefined); -const initialState: State = { - isValid: undefined, - configuration: { - data: { - raw: {}, - format: () => ({} as Mappings), - }, - validate: () => Promise.resolve(false), - }, - properties: { - data: {}, - status: 'idle', - isValid: true, - }, -}; - export interface Props { children: React.ReactNode; + defaultValue: { properties: { [key: string]: any } }; onUpdate: OnUpdateHandler; } -export const MappingsState = ({ children, onUpdate }: Props) => { +export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { + const initialState: State = { + isValid: undefined, + configuration: { + data: { + raw: {}, + format: () => ({} as Mappings), + }, + validate: () => Promise.resolve(false), + }, + properties: { + data: defaultValue.properties, + status: 'idle', + isValid: true, + }, + }; + const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { // console.log('State update', state); @@ -112,7 +113,7 @@ export const MappingsState = ({ children, onUpdate }: Props) => { {children} ); -}; +}); export const useState = () => { const ctx = useContext(StateContext); From f4d4e7f3902b890517dc8c168d920e7c5f7d7b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 30 Sep 2019 15:16:11 +0200 Subject: [PATCH 43/64] Move reducer to its own file --- .../configuration_form/configuration_form.tsx | 6 +- .../ui/components/mappings_editor/index.ts | 2 +- .../mappings_editor/mappings_editor.tsx | 8 +- .../mappings_editor/mappings_state.tsx | 61 +++---------- .../ui/components/mappings_editor/reducer.ts | 90 +++++++++++++++++++ 5 files changed, 108 insertions(+), 59 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx index 3ffb1b0e46987..400934d948669 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -8,9 +8,11 @@ import React, { useEffect } from 'react'; import { useForm, getUseField, Form, OnFormUpdateArg } from '../../shared_imports'; import { FormRow, Field } from '../../shared_imports'; import { DYNAMIC_SETTING_OPTIONS } from '../../field_configuration'; -import { MappingsConfiguration, useDispatch } from '../../mappings_state'; +import { Types, useDispatch } from '../../mappings_state'; import { schema } from './form.schema'; +type MappingsConfiguration = Types['MappingsConfiguration']; + export type ConfigurationUpdateHandler = (arg: OnFormUpdateArg) => void; interface Props { @@ -25,7 +27,7 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { useEffect(() => { const subscription = form.subscribe(updatedConfiguration => { - dispatch({ type: 'updateConfiguration', value: updatedConfiguration }); + dispatch({ type: 'configuration.update', value: updatedConfiguration }); }); return subscription.unsubscribe; }, [form]); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts index 4c178b2a82fd1..8b0e8b7612cc0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/index.ts @@ -6,4 +6,4 @@ export * from './mappings_editor'; -export { OnUpdateHandler, Mappings } from './mappings_state'; +export { OnUpdateHandler, Types } from './mappings_state'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index c6cef8e21cd76..16b5bf409666f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -8,11 +8,7 @@ import React from 'react'; import { ConfigurationForm, CONFIGURATION_FIELDS, DocumentFields } from './components'; -import { - MappingsState, - Props as MappingsStateProps, - MappingsConfiguration, -} from './mappings_state'; +import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state'; interface Props { onUpdate: MappingsStateProps['onUpdate']; @@ -27,7 +23,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props ...acc, [key]: value, }), - {} as MappingsConfiguration + {} as Types['MappingsConfiguration'] ); const propertiesDefaultValue = defaultValue.properties || {}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index 8c76394dc2387..a5c24c07fa791 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -6,23 +6,18 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; -import { OnFormUpdateArg } from './shared_imports'; +import { reducer, MappingsConfiguration, MappingsProperties, State, Dispatch } from './reducer'; -export interface MappingsConfiguration { - dynamic: boolean | string; - date_detection: boolean; - numeric_detection: boolean; - dynamic_date_formats: string[]; -} - -export interface MappingsProperties { - [key: string]: any; -} - -export type Mappings = MappingsConfiguration & { +type Mappings = MappingsConfiguration & { properties: MappingsProperties; }; +export interface Types { + Mappings: Mappings; + MappingsConfiguration: MappingsConfiguration; + MappingsProperties: MappingsProperties; +} + export interface OnUpdateHandlerArg { isValid?: boolean; getData: () => Mappings; @@ -31,42 +26,6 @@ export interface OnUpdateHandlerArg { export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; -interface State { - isValid: boolean | undefined; - configuration: OnFormUpdateArg; - properties: { - data: { [key: string]: any }; - status: 'idle' | 'editing' | 'creating'; - isValid: boolean | undefined; - }; -} - -type Action = - | { type: 'updateConfiguration'; value: OnFormUpdateArg } - | { type: 'addProperty'; value: any }; - -type Dispatch = (action: Action) => void; - -const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'updateConfiguration': - const isValid = - action.value.isValid === undefined || state.properties.isValid === undefined - ? undefined - : action.value.isValid && state.properties.isValid; - - return { - ...state, - isValid, - configuration: action.value, - }; - case 'addProperty': - return state; - default: - throw new Error(`Action "${action!.type}" not recognized.`); - } -}; - const StateContext = createContext(undefined); const DispatchContext = createContext(undefined); @@ -88,9 +47,11 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, properties: { data: defaultValue.properties, - status: 'idle', isValid: true, }, + documentFields: { + status: 'idle', + }, }; const [state, dispatch] = useReducer(reducer, initialState); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts new file mode 100644 index 0000000000000..27b7d124558d9 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -0,0 +1,90 @@ +/* + * 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 { OnFormUpdateArg } from './shared_imports'; + +export interface MappingsConfiguration { + dynamic: boolean | string; + date_detection: boolean; + numeric_detection: boolean; + dynamic_date_formats: string[]; +} + +export interface MappingsProperties { + [key: string]: any; +} + +type DocumentFieldsStatus = 'idle' | 'editingProperty' | 'creatingProperty'; + +export interface State { + isValid: boolean | undefined; + configuration: OnFormUpdateArg; + documentFields: { + status: DocumentFieldsStatus; + propertyToEdit?: string; + }; + properties: { + data: { [key: string]: any }; + isValid: boolean | undefined; + }; +} + +export type Action = + | { type: 'configuration.update'; value: OnFormUpdateArg } + | { type: 'property.add'; value: any } + | { type: 'property.remove'; value: any } + | { type: 'property.edit'; value: any } + | { type: 'documentField.createProperty' } + | { type: 'documentField.editProperty'; value: string } + | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }; + +export type Dispatch = (action: Action) => void; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'configuration.update': + const isValid = + action.value.isValid === undefined || state.properties.isValid === undefined + ? undefined + : action.value.isValid && state.properties.isValid; + + return { + ...state, + isValid, + configuration: action.value, + }; + case 'documentField.createProperty': + return { ...state, documentFields: { ...state.documentFields, status: 'creatingProperty' } }; + case 'documentField.editProperty': + return { + ...state, + documentFields: { + ...state.documentFields, + status: 'editingProperty', + propertyToEdit: action.value, + }, + }; + case 'documentField.changeStatus': + return { ...state, documentFields: { ...state.documentFields, status: action.value } }; + case 'property.add': { + const properties = state.properties.data; // Todo update this to merge new prop + return { + ...state, + properties: { ...state.properties, data: properties }, + documentFields: { ...state.documentFields, status: 'idle' }, + }; + } + case 'property.edit': { + const properties = state.properties.data; // Todo update this to merge new prop + return { + ...state, + properties: { ...state.properties, data: properties }, + documentFields: { ...state.documentFields, status: 'idle' }, + }; + } + default: + throw new Error(`Action "${action!.type}" not recognized.`); + } +}; From 1128dbe36f039e92d048560bb57ce1e53ea4df11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 2 Oct 2019 19:29:46 +0200 Subject: [PATCH 44/64] Add create property and child property --- .../configuration_form/configuration_form.tsx | 2 +- .../configuration_form/form.schema.ts | 2 +- .../document_fields/document_fields.tsx | 13 +-- .../document_fields_header.tsx | 7 +- .../components/document_fields/properties.tsx | 26 ----- .../properties/create_property.tsx | 66 +++++++++++++ .../document_fields/properties/index.ts | 11 +++ .../properties/properties_list.tsx | 84 ++++++++++++++++ .../properties/properties_list_item.tsx | 92 ++++++++++++++++++ .../components/document_fields/property.tsx | 25 ----- .../constants/data_types_definition.ts | 80 ++++++++++++++++ .../constants/field_options.ts | 20 ++++ .../mappings_editor/constants/index.ts | 9 ++ .../field_configuration/field_options.ts | 36 ------- .../{field_configuration => lib}/index.ts | 2 +- .../components/mappings_editor/lib/utils.ts | 70 ++++++++++++++ .../mappings_editor/mappings_editor.tsx | 2 +- .../mappings_editor/mappings_state.tsx | 3 +- .../ui/components/mappings_editor/reducer.ts | 66 ++++++++++++- .../mappings_editor/shared_imports.ts | 2 + .../ui/components/mappings_editor/types.ts | 95 +++++++++++++++++++ 21 files changed, 609 insertions(+), 104 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/data_types_definition.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/field_options.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/{field_configuration => lib}/index.ts (88%) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx index 400934d948669..dc9ae6c158242 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -7,7 +7,7 @@ import React, { useEffect } from 'react'; import { useForm, getUseField, Form, OnFormUpdateArg } from '../../shared_imports'; import { FormRow, Field } from '../../shared_imports'; -import { DYNAMIC_SETTING_OPTIONS } from '../../field_configuration'; +import { DYNAMIC_SETTING_OPTIONS } from '../../constants'; import { Types, useDispatch } from '../../mappings_state'; import { schema } from './form.schema'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts index 6c6d7af651414..96b7161e625d9 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts @@ -5,7 +5,7 @@ */ import { FormSchema, FIELD_TYPES, VALIDATION_TYPES, fieldValidators } from '../../shared_imports'; -import { MappingsConfiguration } from '../../mappings_state'; +import { MappingsConfiguration } from '../../reducer'; const { containsCharsField } = fieldValidators; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index 7d7c07787d40a..b44e38599bc6e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -6,18 +6,19 @@ import React from 'react'; +import { useState } from '../../mappings_state'; import { DocumentFieldsHeaders } from './document_fields_header'; -import { Properties, Props as PropertiesProps } from './properties'; +import { PropertiesList } from './properties'; -interface Props { - defaultValue?: PropertiesProps['defaultValue']; -} +export const DocumentFields = () => { + const { + properties: { data }, + } = useState(); -export const DocumentFields = ({ defaultValue }: Props) => { return ( <> - + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx index 7ebde00f26502..e3cade8743610 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields_header.tsx @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { EuiTitle } from '@elastic/eui'; export const DocumentFieldsHeaders = () => { - return

          Document fields

          ; + return ( + +

          Document fields

          +
          + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx deleted file mode 100644 index 63c3a22f39c9b..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties.tsx +++ /dev/null @@ -1,26 +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 React from 'react'; - -import { Property, IProperty } from './property'; - -export interface Props { - defaultValue?: { [key: string]: Omit }; -} - -export const Properties = ({ defaultValue = {} }: Props) => { - const propertiesArray = Object.entries(defaultValue).map(([name, value]) => ({ name, ...value })); - - return ( -
            - {propertiesArray.map(prop => ( -
          • - -
          • - ))} -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx new file mode 100644 index 0000000000000..6b0f6be386825 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + useForm, + Form, + TextField, + SelectField, + UseField, + fieldValidators, +} from '../../../shared_imports'; +import { FIELD_TYPES_OPTIONS } from '../../../constants'; +import { useDispatch } from '../../../mappings_state'; + +export const CreateProperty = () => { + const { form } = useForm(); + const dispatch = useDispatch(); + + const saveProperty = async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + dispatch({ type: 'property.add', value: data }); + } + }; + + const cancel = () => { + dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + }; + + return ( +
          + + + + + + + + + Save + + + Cancel + + +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts new file mode 100644 index 0000000000000..75e6dac5de4ad --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './properties_list'; + +export * from './properties_list_item'; + +export * from './create_property'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx new file mode 100644 index 0000000000000..ef7b49118d500 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; + +import { useState, useDispatch } from '../../../mappings_state'; +import { PropertiesListItem } from './properties_list_item'; +import { CreateProperty } from './create_property'; +import { Property } from '../../../types'; + +interface Props { + properties?: { [key: string]: Omit }; + path?: string; + treeDepth?: number; +} + +export const PropertiesList = React.memo(({ properties = {}, treeDepth = 0, path = '' }: Props) => { + const propertiesToArray = useMemo(() => { + const array = Object.entries(properties).map( + ([name, value]) => ({ name, ...value } as Property) + ); + if (treeDepth === 0) { + return array; + } + return array.reverse(); // reverse so the new properties added appear on top of the list + }, [properties]); + + const dispatch = useDispatch(); + const { + documentFields: { status, fieldPathToAddProperty }, + } = useState(); + + const addField = () => { + dispatch({ type: 'documentField.createProperty' }); + }; + + const renderCreateProperty = () => { + if (status !== 'creatingProperty') { + return null; + } + + // Root level (0) has does not have the "fieldPathToAddProperty" set + if (treeDepth === 0 && fieldPathToAddProperty !== undefined) { + return null; + } + + return ; + }; + + const renderAddFieldButton = () => { + if (status === 'creatingProperty') { + return null; + } + return ( + <> + + Add field + + ); + }; + + return ( + <> +
          +
            + {propertiesToArray.map(property => ( +
          • + +
          • + ))} +
          +
          + {treeDepth === 0 && ( + <> + {renderCreateProperty()} + {renderAddFieldButton()} + + )} + + ); +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx new file mode 100644 index 0000000000000..7de1cdabfd1b8 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx @@ -0,0 +1,92 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; + +import { useState, useDispatch } from '../../../mappings_state'; +import { PropertiesList } from './properties_list'; +import { CreateProperty } from './create_property'; +import { Property } from '../../../types'; +import { getPropertyMeta } from '../../../lib'; + +interface Props { + property: Property; + parentPath: string; + treeDepth?: number; +} + +export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Props) => { + const dispatch = useDispatch(); + const { + documentFields: { status, fieldPathToAddProperty }, + } = useState(); + const propertyPath = parentPath ? `${parentPath}.${property.name}` : property.name; + const { canHaveChildProperties, childProperties, hasChildProperties } = getPropertyMeta(property); + + const addField = () => { + dispatch({ + type: 'documentField.createProperty', + value: propertyPath, + }); + }; + + const editField = () => { + // console.log('Editing', propertyPath); + }; + + const removeField = () => { + // console.log('Removing', propertyPath); + }; + + // console.log('............................Rendering', propertyPath); + + const renderCreateProperty = () => { + if (status !== 'creatingProperty') { + return null; + } + + // Root level (0) has does not have the "fieldPathToAddProperty" set + if (fieldPathToAddProperty !== propertyPath) { + return null; + } + + return ; + }; + + const renderActionButtons = () => { + if (status !== 'idle') { + return null; + } + + return ( + <> + Edit + {canHaveChildProperties && Add field} + Remove + + ); + }; + + return ( +
          +
          + {property.name} | {property.type} {renderActionButtons()} +
          + + {renderCreateProperty()} + + {hasChildProperties && ( +
          + +
          + )} +
          + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx deleted file mode 100644 index b39224d973dbb..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/property.tsx +++ /dev/null @@ -1,25 +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 React from 'react'; - -export interface IProperty { - name: string; - type: string; -} - -interface Props { - value: IProperty; -} - -export const Property = ({ value }: Props) => { - return ( -
          -

          - {value.name} | {value.type} -

          -
          - ); -}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/data_types_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/data_types_definition.ts new file mode 100644 index 0000000000000..0ddced236c0f4 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/data_types_definition.ts @@ -0,0 +1,80 @@ +/* + * 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 { MainType, DataTypeDefinition } from '../types'; + +export const DATA_TYPE_DEFINITION: { [key in MainType]: DataTypeDefinition } = { + text: { + label: 'Text', + basicParameters: ['store', 'index', 'fielddata'], + }, + keyword: { + label: 'Keyword', + basicParameters: ['store', 'index', 'doc_values'], + }, + numeric: { + label: 'Numeric', + subTypes: { + label: 'Numeric type', + types: ['long', 'integer', 'short', 'byte', 'double', 'float', 'half_float', 'scaled_float'], + }, + basicParameters: [ + ['store', 'index', 'coerce', 'doc_values', 'ignore_malformed'], + ['null_value', 'boost'], + ], + }, + date: { + label: 'Date', + subTypes: { + label: 'Date type', + types: ['date', 'date_nanos'], + }, + basicParameters: [ + ['store', 'index', 'doc_values', 'ignore_malformed'], + ['null_value', 'boost', 'locale', 'format'], + ], + }, + binary: { + label: 'Binary', + basicParameters: ['doc_values', 'store'], + }, + ip: { + label: 'IP', + basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], + }, + boolean: { + label: 'Boolean', + basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], + }, + range: { + label: 'Range', + subTypes: { + label: 'Range type', + types: ['integer_range', 'float_range', 'long_range', 'double_range', 'date_range'], + }, + basicParameters: [['store', 'index', 'coerce', 'doc_values'], ['boost']], + }, + object: { + label: 'Object', + basicParameters: ['dynamic', 'enabled'], + }, + nested: { + label: 'Nested', + basicParameters: ['dynamic'], + }, + rank_feature: { + label: 'Rank feature', + }, + rank_features: { + label: 'Rank features', + }, + dense_vector: { + label: 'Dense vector', + }, + sparse_vector: { + label: 'Sparse vector', + }, +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/field_options.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/field_options.ts new file mode 100644 index 0000000000000..c60fedcfb12ab --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/field_options.ts @@ -0,0 +1,20 @@ +/* + * 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 { DATA_TYPE_DEFINITION } from './data_types_definition'; + +export const DYNAMIC_SETTING_OPTIONS = [ + { value: true, text: 'true' }, + { value: false, text: 'false' }, + { value: 'strict', text: 'strict' }, +]; + +export const FIELD_TYPES_OPTIONS = Object.entries(DATA_TYPE_DEFINITION).map( + ([dataType, { label }]) => ({ + value: dataType, + text: label, + }) +); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts new file mode 100644 index 0000000000000..07d95ae11ec81 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './field_options'; + +export * from './data_types_definition'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts deleted file mode 100644 index b5005f5f390d7..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/field_options.ts +++ /dev/null @@ -1,36 +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 const DYNAMIC_SETTING_OPTIONS = [ - { value: true, text: 'true' }, - { value: false, text: 'false' }, - { value: 'strict', text: 'strict' }, -]; - -// export const ANALYZERS_OPTIONS = [ -// { value: 'index_default', text: 'Index default' }, -// { value: 'standard', text: 'Standard' }, -// { value: 'simple', text: 'Simple' }, -// { value: 'whitespace', text: 'Whitespace' }, -// { value: 'stop', text: 'Stop' }, -// { value: 'keyword', text: 'Keyword' }, -// { value: 'pattern', text: 'Pattern' }, -// { value: 'language', text: 'Language' }, -// { value: 'fingerprint', text: 'Fingerprint' }, -// ]; - -// export const INDEX_OPTIONS = [ -// { value: 'docs', text: 'Docs' }, -// { value: 'freqs', text: 'Freqs' }, -// { value: 'positions', text: 'Positions' }, -// { value: 'offsets', text: 'Offsets' }, -// ]; - -// export const SIMILARITY_ALGORITHM_OPTIONS = [ -// { value: 'BM25', text: 'BM25' }, -// { value: 'classic', text: 'classic' }, -// { value: 'boolean', text: 'boolean' }, -// ]; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/index.ts similarity index 88% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/index.ts index 78dab4b754908..f7db56edcc28d 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/field_configuration/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './field_options'; +export * from './utils'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts new file mode 100644 index 0000000000000..926ed7bed6207 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -0,0 +1,70 @@ +/* + * 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 { DataType, Property, MainType, SubType } from '../types'; +import { DATA_TYPE_DEFINITION } from '../constants'; + +export const hasNestedProperties = (dataType: DataType) => + dataType === 'object' || dataType === 'nested' || dataType === 'text' || dataType === 'keyword'; + +const getChildPropertiesAttributeName = (dataType: DataType) => { + if (dataType === 'text' || dataType === 'keyword') { + return 'fields'; + } else if (dataType === 'object' || dataType === 'nested') { + return 'properties'; + } + return undefined; +}; + +interface PropertyMeta { + childPropertiesName: 'fields' | 'properties' | undefined; + canHaveChildProperties: boolean; + hasChildProperties: boolean; + childProperties: Record | undefined; +} + +export const getPropertyMeta = (property: Property): PropertyMeta => { + const childPropertiesName = getChildPropertiesAttributeName(property.type); + const canHaveChildProperties = Boolean(childPropertiesName); + const hasChildProperties = + childPropertiesName !== undefined && + Boolean(property[childPropertiesName]) && + Object.keys(property[childPropertiesName]!).length > 0; + + const childProperties = canHaveChildProperties ? property[childPropertiesName!] : undefined; + return { + hasChildProperties, + childPropertiesName, + canHaveChildProperties, + childProperties, + }; +}; + +/** + * Return a map of subType -> mainType + * + * @example + * + * { + * long: 'numeric', + * integer: 'numeric', + * short: 'numeric', + * } + */ +const subTypesMapToType = Object.entries(DATA_TYPE_DEFINITION).reduce( + (acc, [type, definition]) => { + if ({}.hasOwnProperty.call(definition, 'subTypes')) { + definition.subTypes!.types.forEach(subType => { + acc[subType] = type; + }); + } + return acc; + }, + {} as Record +); + +export const getTypeFromSubType = (subType: SubType): MainType => + subTypesMapToType[subType] as MainType; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 16b5bf409666f..115c0edf70102 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -30,7 +30,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props return ( - + ); }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index a5c24c07fa791..5b95a8d71eb59 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -7,6 +7,7 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; import { reducer, MappingsConfiguration, MappingsProperties, State, Dispatch } from './reducer'; +import { Property } from './types'; type Mappings = MappingsConfiguration & { properties: MappingsProperties; @@ -31,7 +32,7 @@ const DispatchContext = createContext(undefined); export interface Props { children: React.ReactNode; - defaultValue: { properties: { [key: string]: any } }; + defaultValue: { properties: { [key: string]: Property } }; onUpdate: OnUpdateHandler; } diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 27b7d124558d9..4cef7513b4947 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; +import { Properties, Property } from './types'; +import { getPropertyMeta } from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -24,9 +26,10 @@ export interface State { documentFields: { status: DocumentFieldsStatus; propertyToEdit?: string; + fieldPathToAddProperty?: string; }; properties: { - data: { [key: string]: any }; + data: Properties; isValid: boolean | undefined; }; } @@ -36,12 +39,39 @@ export type Action = | { type: 'property.add'; value: any } | { type: 'property.remove'; value: any } | { type: 'property.edit'; value: any } - | { type: 'documentField.createProperty' } + | { type: 'documentField.createProperty'; value?: string } | { type: 'documentField.editProperty'; value: string } | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }; export type Dispatch = (action: Action) => void; +const getChildProperty = ( + property: Property, + pathsArray: string[] +): { property: Property; childPropertiesName: 'properties' | 'fields' } => { + const { childPropertiesName } = getPropertyMeta(property); + + if (!Boolean(pathsArray.length)) { + return { property, childPropertiesName: childPropertiesName! }; + } + + // Clone the "properties" or "fields" object + property[childPropertiesName!] = { + ...property[childPropertiesName!], + }; + + // Access the child property at next path + const childProperty = property[childPropertiesName!]![pathsArray[0]]; + + // Recursively access the property + return getChildProperty(childProperty, pathsArray.slice(1)); +}; + +const getPropertyAtPath = (path: string, properties: Properties) => { + const pathArray = path.split('.'); + return getChildProperty(properties[pathArray[0]] as Property, pathArray.slice(1)); +}; + export const reducer = (state: State, action: Action): State => { switch (action.type) { case 'configuration.update': @@ -56,7 +86,14 @@ export const reducer = (state: State, action: Action): State => { configuration: action.value, }; case 'documentField.createProperty': - return { ...state, documentFields: { ...state.documentFields, status: 'creatingProperty' } }; + return { + ...state, + documentFields: { + ...state.documentFields, + fieldPathToAddProperty: action.value, + status: 'creatingProperty', + }, + }; case 'documentField.editProperty': return { ...state, @@ -69,10 +106,29 @@ export const reducer = (state: State, action: Action): State => { case 'documentField.changeStatus': return { ...state, documentFields: { ...state.documentFields, status: action.value } }; case 'property.add': { - const properties = state.properties.data; // Todo update this to merge new prop + const { name, ...rest } = action.value; + const { fieldPathToAddProperty } = state.documentFields; + + const updatedPropertiesData = { ...state.properties.data }; + + if (fieldPathToAddProperty === undefined) { + // Adding at the root level + updatedPropertiesData[name] = rest; + } else { + const { property, childPropertiesName } = getPropertyAtPath( + fieldPathToAddProperty, + updatedPropertiesData + ); + + property[childPropertiesName] = { + ...property[childPropertiesName], + [name]: rest, + }; + } + return { ...state, - properties: { ...state.properties, data: properties }, + properties: { ...state.properties, data: updatedPropertiesData }, documentFields: { ...state.documentFields, status: 'idle' }, }; } diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts index 38c04c75662a0..5ea8f0952222b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts @@ -18,6 +18,8 @@ export { export { FormRow, Field, + TextField, + SelectField, } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts new file mode 100644 index 0000000000000..a6e5f729813e4 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -0,0 +1,95 @@ +/* + * 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 DataTypeDefinition { + label: string; + subTypes?: { label: string; types: SubType[] }; + configuration?: ParameterName[]; + basicParameters?: ParameterName[] | ParameterName[][]; + hasAdvancedParameters?: boolean; + hasMultiFields?: boolean; +} + +export type MainType = + | 'text' + | 'keyword' + | 'numeric' + | 'date' + | 'binary' + | 'boolean' + | 'range' + | 'object' + | 'nested' + | 'ip' + | 'rank_feature' + | 'rank_features' + | 'dense_vector' + | 'sparse_vector'; + +export type SubType = NumericType | DateType | RangeType; + +export type DataType = MainType | SubType; + +export type NumericType = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'double' + | 'float' + | 'half_float' + | 'scaled_float'; + +export type DateType = 'date' | 'date_nanos'; + +export type RangeType = + | 'integer_range' + | 'float_range' + | 'long_range' + | 'double_range' + | 'date_range'; + +export type ParameterName = + | 'name' + | 'type' + | 'store' + | 'index' + | 'fielddata' + | 'doc_values' + | 'coerce' + | 'ignore_malformed' + | 'null_value' + | 'dynamic' + | 'enabled' + | 'boost' + | 'locale' + | 'format' + | 'analyzer' + | 'search_analyzer' + | 'search_quote_analyzer' + | 'index_options' + | 'eager_global_ordinals' + | 'index_prefixes' + | 'index_phrases' + | 'norms' + | 'term_vector' + | 'position_increment_gap' + | 'similarity' + | 'normalizer' + | 'ignore_above' + | 'split_queries_on_whitespace'; + +export interface Properties { + [key: string]: Property; +} + +export interface Property { + name: string; + type: DataType; + properties?: { [key: string]: Property }; + fields?: { [key: string]: Property }; + [key: string]: any; +} From e4ee1bd123b22912eebe7a21c41d53fe7e41844e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 2 Oct 2019 21:10:02 +0200 Subject: [PATCH 45/64] Normalize properties to 1 level deep object --- .../document_fields/document_fields.tsx | 7 ++- .../properties/properties_list.tsx | 18 ++----- .../properties/properties_list_item.tsx | 12 +++-- .../components/mappings_editor/lib/utils.ts | 49 ++++++++++++++++++- .../mappings_editor/mappings_state.tsx | 7 ++- .../ui/components/mappings_editor/reducer.ts | 47 +++++++++--------- .../ui/components/mappings_editor/types.ts | 1 + 7 files changed, 95 insertions(+), 46 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index b44e38599bc6e..2291928caa0be 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -12,13 +12,16 @@ import { PropertiesList } from './properties'; export const DocumentFields = () => { const { - properties: { data }, + properties: { byId, topLevelFields }, } = useState(); + const getProperty = (propId: string) => byId[propId]; + const properties = topLevelFields.map(getProperty); + return ( <> - + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx index ef7b49118d500..830c06f0577ed 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx @@ -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, { useMemo } from 'react'; +import React from 'react'; import { EuiButton, EuiSpacer } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; @@ -12,22 +12,12 @@ import { CreateProperty } from './create_property'; import { Property } from '../../../types'; interface Props { - properties?: { [key: string]: Omit }; + properties?: Property[]; path?: string; treeDepth?: number; } -export const PropertiesList = React.memo(({ properties = {}, treeDepth = 0, path = '' }: Props) => { - const propertiesToArray = useMemo(() => { - const array = Object.entries(properties).map( - ([name, value]) => ({ name, ...value } as Property) - ); - if (treeDepth === 0) { - return array; - } - return array.reverse(); // reverse so the new properties added appear on top of the list - }, [properties]); - +export const PropertiesList = React.memo(({ properties = [], treeDepth = 0, path = '' }: Props) => { const dispatch = useDispatch(); const { documentFields: { status, fieldPathToAddProperty }, @@ -66,7 +56,7 @@ export const PropertiesList = React.memo(({ properties = {}, treeDepth = 0, path <>
            - {propertiesToArray.map(property => ( + {properties.map(property => (
          • diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx index 7de1cdabfd1b8..2cd21e3a09ef0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx @@ -22,9 +22,15 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop const dispatch = useDispatch(); const { documentFields: { status, fieldPathToAddProperty }, + properties: { byId }, } = useState(); const propertyPath = parentPath ? `${parentPath}.${property.name}` : property.name; - const { canHaveChildProperties, childProperties, hasChildProperties } = getPropertyMeta(property); + const { canHaveChildProperties } = getPropertyMeta(property); + const getProperty = (propId: string) => byId[propId]; + const hasChildProperties = property.__childProperties__ !== undefined; + const childProperties = hasChildProperties + ? property.__childProperties__!.map(getProperty) + : null; const addField = () => { dispatch({ @@ -41,8 +47,6 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop // console.log('Removing', propertyPath); }; - // console.log('............................Rendering', propertyPath); - const renderCreateProperty = () => { if (status !== 'creatingProperty') { return null; @@ -81,7 +85,7 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop {hasChildProperties && (
            diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 926ed7bed6207..6aabd71e684e5 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataType, Property, MainType, SubType } from '../types'; +import { DataType, Properties, Property, MainType, SubType } from '../types'; import { DATA_TYPE_DEFINITION } from '../constants'; export const hasNestedProperties = (dataType: DataType) => @@ -68,3 +68,50 @@ const subTypesMapToType = Object.entries(DATA_TYPE_DEFINITION).reduce( export const getTypeFromSubType = (subType: SubType): MainType => subTypesMapToType[subType] as MainType; + +const isObject = (value: any): boolean => + value !== null && !Array.isArray(value) && typeof value === 'object'; + +export interface NormalizedProperties { + byId: { [id: string]: Property }; + topLevelFields: string[]; +} + +export const normalize = (propertiesToNormalize: Properties): NormalizedProperties => { + const normalizeObject = ( + props: Properties = propertiesToNormalize, + to: { [id: string]: Property } = {}, + paths: string[] = [] + ): Record => + Object.entries(props).reduce((acc, [propName, value]) => { + const updatedPaths = [...paths, propName]; + const propertyPath = updatedPaths.join('.'); + const property = { name: propName, ...value } as any; + + const { properties, fields, ...rest } = property; + + if (isObject(property.properties)) { + acc[updatedPaths.join('.')] = { + ...rest, + __childProperties__: Object.keys(properties).map(key => `${propertyPath}.${key}`), + }; + return normalizeObject(properties, to, updatedPaths); + } else if (isObject(property.fields)) { + acc[updatedPaths.join('.')] = { + ...rest, + __childProperties__: Object.keys(fields).map(key => `${propertyPath}.${key}`), + }; + return normalizeObject(fields, to, updatedPaths); + } + + acc[updatedPaths.join('.')] = property; + return acc; + }, to); + + const byId = normalizeObject(); + + return { + byId, + topLevelFields: Object.keys(propertiesToNormalize), + }; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index 5b95a8d71eb59..3893edfbc5e26 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -8,6 +8,7 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; import { reducer, MappingsConfiguration, MappingsProperties, State, Dispatch } from './reducer'; import { Property } from './types'; +import { normalize } from './lib'; type Mappings = MappingsConfiguration & { properties: MappingsProperties; @@ -37,6 +38,7 @@ export interface Props { } export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { + const { byId, topLevelFields } = normalize(defaultValue.properties); const initialState: State = { isValid: undefined, configuration: { @@ -47,7 +49,8 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P validate: () => Promise.resolve(false), }, properties: { - data: defaultValue.properties, + byId, + topLevelFields, isValid: true, }, documentFields: { @@ -61,7 +64,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P onUpdate({ getData: () => ({ ...state.configuration.data.format(), - properties: state.properties.data, + properties: state.properties.byId, }), validate: () => { return state.configuration.validate(); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 4cef7513b4947..5ae777071f395 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; -import { Properties, Property } from './types'; -import { getPropertyMeta } from './lib'; +import { Property } from './types'; +import { getPropertyMeta, NormalizedProperties } from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -29,7 +29,8 @@ export interface State { fieldPathToAddProperty?: string; }; properties: { - data: Properties; + byId: NormalizedProperties['byId']; + topLevelFields: NormalizedProperties['topLevelFields']; isValid: boolean | undefined; }; } @@ -67,11 +68,6 @@ const getChildProperty = ( return getChildProperty(childProperty, pathsArray.slice(1)); }; -const getPropertyAtPath = (path: string, properties: Properties) => { - const pathArray = path.split('.'); - return getChildProperty(properties[pathArray[0]] as Property, pathArray.slice(1)); -}; - export const reducer = (state: State, action: Action): State => { switch (action.type) { case 'configuration.update': @@ -106,37 +102,42 @@ export const reducer = (state: State, action: Action): State => { case 'documentField.changeStatus': return { ...state, documentFields: { ...state.documentFields, status: action.value } }; case 'property.add': { - const { name, ...rest } = action.value; const { fieldPathToAddProperty } = state.documentFields; + const { name } = action.value; + const propId = + fieldPathToAddProperty === undefined ? name : `${fieldPathToAddProperty}.${name}`; - const updatedPropertiesData = { ...state.properties.data }; + const byId: NormalizedProperties['byId'] = { + ...state.properties.byId, + [propId]: action.value, + }; + let topLevelFields = state.properties.topLevelFields; if (fieldPathToAddProperty === undefined) { - // Adding at the root level - updatedPropertiesData[name] = rest; + // update topLevel fields array + topLevelFields = [...topLevelFields, name]; } else { - const { property, childPropertiesName } = getPropertyAtPath( - fieldPathToAddProperty, - updatedPropertiesData - ); - - property[childPropertiesName] = { - ...property[childPropertiesName], - [name]: rest, + const parentProperty = state.properties.byId[fieldPathToAddProperty!]; + const childProperties = parentProperty.__childProperties__ || []; + + // Update parent property with new children + byId[fieldPathToAddProperty] = { + ...parentProperty, + __childProperties__: [propId, ...childProperties], }; } return { ...state, - properties: { ...state.properties, data: updatedPropertiesData }, + properties: { ...state.properties, byId, topLevelFields }, documentFields: { ...state.documentFields, status: 'idle' }, }; } case 'property.edit': { - const properties = state.properties.data; // Todo update this to merge new prop + // const properties = state.properties.data; // Todo update this to merge new prop return { ...state, - properties: { ...state.properties, data: properties }, + // properties: { ...state.properties, data: properties }, documentFields: { ...state.documentFields, status: 'idle' }, }; } diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index a6e5f729813e4..9cfa0e508c572 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -91,5 +91,6 @@ export interface Property { type: DataType; properties?: { [key: string]: Property }; fields?: { [key: string]: Property }; + __childProperties__?: string[]; [key: string]: any; } From 5a28df1226f70eb62e2bbeb0eb5be5d2708a0e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 2 Oct 2019 22:26:22 +0200 Subject: [PATCH 46/64] Rename topLevelFields --> rootLevelFields --- .../document_fields/document_fields.tsx | 4 ++-- .../components/mappings_editor/lib/utils.ts | 4 ++-- .../mappings_editor/mappings_state.tsx | 4 ++-- .../ui/components/mappings_editor/reducer.ts | 20 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index 2291928caa0be..859b7ed46fcd9 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -12,11 +12,11 @@ import { PropertiesList } from './properties'; export const DocumentFields = () => { const { - properties: { byId, topLevelFields }, + properties: { byId, rootLevelFields }, } = useState(); const getProperty = (propId: string) => byId[propId]; - const properties = topLevelFields.map(getProperty); + const properties = rootLevelFields.map(getProperty); return ( <> diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 6aabd71e684e5..a8cae9d749602 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -74,7 +74,7 @@ const isObject = (value: any): boolean => export interface NormalizedProperties { byId: { [id: string]: Property }; - topLevelFields: string[]; + rootLevelFields: string[]; } export const normalize = (propertiesToNormalize: Properties): NormalizedProperties => { @@ -112,6 +112,6 @@ export const normalize = (propertiesToNormalize: Properties): NormalizedProperti return { byId, - topLevelFields: Object.keys(propertiesToNormalize), + rootLevelFields: Object.keys(propertiesToNormalize), }; }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index 3893edfbc5e26..e25597a370151 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -38,7 +38,7 @@ export interface Props { } export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { - const { byId, topLevelFields } = normalize(defaultValue.properties); + const { byId, rootLevelFields } = normalize(defaultValue.properties); const initialState: State = { isValid: undefined, configuration: { @@ -50,7 +50,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, properties: { byId, - topLevelFields, + rootLevelFields, isValid: true, }, documentFields: { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 5ae777071f395..b68dad47a4683 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -30,7 +30,7 @@ export interface State { }; properties: { byId: NormalizedProperties['byId']; - topLevelFields: NormalizedProperties['topLevelFields']; + rootLevelFields: NormalizedProperties['rootLevelFields']; isValid: boolean | undefined; }; } @@ -104,24 +104,24 @@ export const reducer = (state: State, action: Action): State => { case 'property.add': { const { fieldPathToAddProperty } = state.documentFields; const { name } = action.value; - const propId = - fieldPathToAddProperty === undefined ? name : `${fieldPathToAddProperty}.${name}`; + const addToRootLevel = fieldPathToAddProperty === undefined; + const propId = addToRootLevel ? name : `${fieldPathToAddProperty}.${name}`; + + const rootLevelFields = addToRootLevel + ? [...state.properties.rootLevelFields, name] + : state.properties.rootLevelFields; const byId: NormalizedProperties['byId'] = { ...state.properties.byId, [propId]: action.value, }; - let topLevelFields = state.properties.topLevelFields; - if (fieldPathToAddProperty === undefined) { - // update topLevel fields array - topLevelFields = [...topLevelFields, name]; - } else { + if (!addToRootLevel) { const parentProperty = state.properties.byId[fieldPathToAddProperty!]; const childProperties = parentProperty.__childProperties__ || []; // Update parent property with new children - byId[fieldPathToAddProperty] = { + byId[fieldPathToAddProperty!] = { ...parentProperty, __childProperties__: [propId, ...childProperties], }; @@ -129,7 +129,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, - properties: { ...state.properties, byId, topLevelFields }, + properties: { ...state.properties, byId, rootLevelFields }, documentFields: { ...state.documentFields, status: 'idle' }, }; } From 293d9b0ff6989f99a84eaaf56265ab2aca91974e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 14:59:47 +0200 Subject: [PATCH 47/64] Move property meta to normalize() call --- .../document_fields/document_fields.tsx | 34 +++++++- .../properties/properties_list.tsx | 65 ++------------- .../properties/properties_list_item.tsx | 42 ++++++---- .../components/mappings_editor/lib/utils.ts | 82 +++++++++---------- .../ui/components/mappings_editor/reducer.ts | 39 ++------- .../ui/components/mappings_editor/types.ts | 24 +++++- 6 files changed, 132 insertions(+), 154 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index 859b7ed46fcd9..31a1d27ff2fab 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -5,23 +5,53 @@ */ import React from 'react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; -import { useState } from '../../mappings_state'; +import { useState, useDispatch } from '../../mappings_state'; import { DocumentFieldsHeaders } from './document_fields_header'; -import { PropertiesList } from './properties'; +import { PropertiesList, CreateProperty } from './properties'; export const DocumentFields = () => { + const dispatch = useDispatch(); const { properties: { byId, rootLevelFields }, + documentFields: { status, fieldPathToAddProperty }, } = useState(); const getProperty = (propId: string) => byId[propId]; const properties = rootLevelFields.map(getProperty); + const addField = () => { + dispatch({ type: 'documentField.createProperty' }); + }; + + const renderCreateProperty = () => { + // Root level (0) does not have the "fieldPathToAddProperty" set + if (status !== 'creatingProperty' || fieldPathToAddProperty !== undefined) { + return null; + } + + return ; + }; + + const renderAddFieldButton = () => { + if (status === 'creatingProperty') { + return null; + } + return ( + <> + + Add field + + ); + }; + return ( <> + {renderCreateProperty()} + {renderAddFieldButton()} ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx index 830c06f0577ed..4f9ba04896441 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx @@ -4,71 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiButton, EuiSpacer } from '@elastic/eui'; -import { useState, useDispatch } from '../../../mappings_state'; import { PropertiesListItem } from './properties_list_item'; -import { CreateProperty } from './create_property'; -import { Property } from '../../../types'; +import { NormalizedProperty } from '../../../types'; interface Props { - properties?: Property[]; + properties?: NormalizedProperty[]; path?: string; treeDepth?: number; } export const PropertiesList = React.memo(({ properties = [], treeDepth = 0, path = '' }: Props) => { - const dispatch = useDispatch(); - const { - documentFields: { status, fieldPathToAddProperty }, - } = useState(); - - const addField = () => { - dispatch({ type: 'documentField.createProperty' }); - }; - - const renderCreateProperty = () => { - if (status !== 'creatingProperty') { - return null; - } - - // Root level (0) has does not have the "fieldPathToAddProperty" set - if (treeDepth === 0 && fieldPathToAddProperty !== undefined) { - return null; - } - - return ; - }; - - const renderAddFieldButton = () => { - if (status === 'creatingProperty') { - return null; - } - return ( - <> - - Add field - - ); - }; - return ( - <> -
            -
              - {properties.map(property => ( -
            • - -
            • - ))} -
            -
            - {treeDepth === 0 && ( - <> - {renderCreateProperty()} - {renderAddFieldButton()} - - )} - +
              + {properties.map(property => ( +
            • + +
            • + ))} +
            ); }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx index 2cd21e3a09ef0..4d14935845b19 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx @@ -9,33 +9,35 @@ import { EuiButton } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { PropertiesList } from './properties_list'; import { CreateProperty } from './create_property'; -import { Property } from '../../../types'; -import { getPropertyMeta } from '../../../lib'; +import { NormalizedProperty } from '../../../types'; interface Props { - property: Property; + property: NormalizedProperty; parentPath: string; treeDepth?: number; } +const inlineStyle = { + padding: '20px 0', + borderBottom: '1px solid #ddd', + height: '82px', + display: 'flex', + alignItems: 'center', +}; + export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { documentFields: { status, fieldPathToAddProperty }, properties: { byId }, } = useState(); - const propertyPath = parentPath ? `${parentPath}.${property.name}` : property.name; - const { canHaveChildProperties } = getPropertyMeta(property); const getProperty = (propId: string) => byId[propId]; - const hasChildProperties = property.__childProperties__ !== undefined; - const childProperties = hasChildProperties - ? property.__childProperties__!.map(getProperty) - : null; + const { path, resource, childProperties, hasChildProperties, canHaveChildProperties } = property; const addField = () => { dispatch({ type: 'documentField.createProperty', - value: propertyPath, + value: path, }); }; @@ -53,11 +55,15 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop } // Root level (0) has does not have the "fieldPathToAddProperty" set - if (fieldPathToAddProperty !== propertyPath) { + if (fieldPathToAddProperty !== path) { return null; } - return ; + return ( +
            + +
            + ); }; const renderActionButtons = () => { @@ -75,9 +81,9 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop }; return ( -
            -
            - {property.name} | {property.type} {renderActionButtons()} + <> +
            + {resource.name} | {resource.type} {renderActionButtons()}
            {renderCreateProperty()} @@ -85,12 +91,12 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop {hasChildProperties && (
            )} -
            + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index a8cae9d749602..8850dc9f1a697 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataType, Properties, Property, MainType, SubType } from '../types'; +import { + DataType, + Properties, + Property, + NormalizedProperties, + NormalizedProperty, + PropertyMeta, + MainType, + SubType, + ChildPropertyName, +} from '../types'; import { DATA_TYPE_DEFINITION } from '../constants'; -export const hasNestedProperties = (dataType: DataType) => - dataType === 'object' || dataType === 'nested' || dataType === 'text' || dataType === 'keyword'; - -const getChildPropertiesAttributeName = (dataType: DataType) => { +const getChildPropertiesName = (dataType: DataType): ChildPropertyName | undefined => { if (dataType === 'text' || dataType === 'keyword') { return 'fields'; } else if (dataType === 'object' || dataType === 'nested') { @@ -19,23 +26,22 @@ const getChildPropertiesAttributeName = (dataType: DataType) => { return undefined; }; -interface PropertyMeta { - childPropertiesName: 'fields' | 'properties' | undefined; - canHaveChildProperties: boolean; - hasChildProperties: boolean; - childProperties: Record | undefined; -} - -export const getPropertyMeta = (property: Property): PropertyMeta => { - const childPropertiesName = getChildPropertiesAttributeName(property.type); +const getPropertyMeta = (property: Property, propertyPath: string): PropertyMeta => { + const childPropertiesName = getChildPropertiesName(property.type); const canHaveChildProperties = Boolean(childPropertiesName); const hasChildProperties = childPropertiesName !== undefined && Boolean(property[childPropertiesName]) && Object.keys(property[childPropertiesName]!).length > 0; - const childProperties = canHaveChildProperties ? property[childPropertiesName!] : undefined; + const childProperties = hasChildProperties + ? Object.keys(property[childPropertiesName!]!).map( + propertyName => `${propertyPath}.${propertyName}` + ) + : undefined; + return { + path: propertyPath, hasChildProperties, childPropertiesName, canHaveChildProperties, @@ -69,46 +75,34 @@ const subTypesMapToType = Object.entries(DATA_TYPE_DEFINITION).reduce( export const getTypeFromSubType = (subType: SubType): MainType => subTypesMapToType[subType] as MainType; -const isObject = (value: any): boolean => - value !== null && !Array.isArray(value) && typeof value === 'object'; - -export interface NormalizedProperties { - byId: { [id: string]: Property }; - rootLevelFields: string[]; -} - export const normalize = (propertiesToNormalize: Properties): NormalizedProperties => { - const normalizeObject = ( - props: Properties = propertiesToNormalize, - to: { [id: string]: Property } = {}, + const normalizeProperties = ( + props: Properties, + to: NormalizedProperties['byId'] = {}, paths: string[] = [] ): Record => Object.entries(props).reduce((acc, [propName, value]) => { - const updatedPaths = [...paths, propName]; - const propertyPath = updatedPaths.join('.'); - const property = { name: propName, ...value } as any; + const propertyPathArray = [...paths, propName]; + const propertyPath = propertyPathArray.join('.'); + const property = { name: propName, ...value } as Property; + const meta = getPropertyMeta(property, propertyPath); + + const normalizedProperty: NormalizedProperty = { + resource: property, + ...meta, + }; - const { properties, fields, ...rest } = property; + acc[propertyPath] = normalizedProperty; - if (isObject(property.properties)) { - acc[updatedPaths.join('.')] = { - ...rest, - __childProperties__: Object.keys(properties).map(key => `${propertyPath}.${key}`), - }; - return normalizeObject(properties, to, updatedPaths); - } else if (isObject(property.fields)) { - acc[updatedPaths.join('.')] = { - ...rest, - __childProperties__: Object.keys(fields).map(key => `${propertyPath}.${key}`), - }; - return normalizeObject(fields, to, updatedPaths); + if (meta.hasChildProperties) { + return normalizeProperties(property[meta.childPropertiesName!]!, to, propertyPathArray); } - acc[updatedPaths.join('.')] = property; + acc[propertyPath] = normalizedProperty; return acc; }, to); - const byId = normalizeObject(); + const byId = normalizeProperties(propertiesToNormalize); return { byId, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index b68dad47a4683..221b2b86e8a61 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; -import { Property } from './types'; -import { getPropertyMeta, NormalizedProperties } from './lib'; +import { NormalizedProperties } from './types'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -46,28 +45,6 @@ export type Action = export type Dispatch = (action: Action) => void; -const getChildProperty = ( - property: Property, - pathsArray: string[] -): { property: Property; childPropertiesName: 'properties' | 'fields' } => { - const { childPropertiesName } = getPropertyMeta(property); - - if (!Boolean(pathsArray.length)) { - return { property, childPropertiesName: childPropertiesName! }; - } - - // Clone the "properties" or "fields" object - property[childPropertiesName!] = { - ...property[childPropertiesName!], - }; - - // Access the child property at next path - const childProperty = property[childPropertiesName!]![pathsArray[0]]; - - // Recursively access the property - return getChildProperty(childProperty, pathsArray.slice(1)); -}; - export const reducer = (state: State, action: Action): State => { switch (action.type) { case 'configuration.update': @@ -111,25 +88,23 @@ export const reducer = (state: State, action: Action): State => { ? [...state.properties.rootLevelFields, name] : state.properties.rootLevelFields; - const byId: NormalizedProperties['byId'] = { - ...state.properties.byId, - [propId]: action.value, - }; + state.properties.byId[propId] = action.value; if (!addToRootLevel) { const parentProperty = state.properties.byId[fieldPathToAddProperty!]; - const childProperties = parentProperty.__childProperties__ || []; + const childProperties = parentProperty.childProperties || []; + // TODO HERE: create a new Set() intead of an empty array? // Update parent property with new children - byId[fieldPathToAddProperty!] = { + state.properties.byId[fieldPathToAddProperty!] = { ...parentProperty, - __childProperties__: [propId, ...childProperties], + childProperties: [propId, ...childProperties], }; } return { ...state, - properties: { ...state.properties, byId, rootLevelFields }, + properties: { ...state.properties, rootLevelFields }, documentFields: { ...state.documentFields, status: 'idle' }, }; } diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index 9cfa0e508c572..64741a2e10313 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -83,7 +83,7 @@ export type ParameterName = | 'split_queries_on_whitespace'; export interface Properties { - [key: string]: Property; + [key: string]: Omit; } export interface Property { @@ -91,6 +91,26 @@ export interface Property { type: DataType; properties?: { [key: string]: Property }; fields?: { [key: string]: Property }; - __childProperties__?: string[]; [key: string]: any; } + +export interface PropertyMeta { + path: string; + childPropertiesName: ChildPropertyName | undefined; + canHaveChildProperties: boolean; + hasChildProperties: boolean; + childProperties: string[] | undefined; +} + +export interface NormalizedProperties { + byId: { + [id: string]: NormalizedProperty; + }; + rootLevelFields: string[]; +} + +export interface NormalizedProperty extends PropertyMeta { + resource: Property; +} + +export type ChildPropertyName = 'properties' | 'fields'; From 22f54a326346849f206ec8a0172038fa940f0c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 15:45:54 +0200 Subject: [PATCH 48/64] Add reset() method to hook form --- .../forms/hook_form_lib/hooks/use_field.ts | 17 ++++++++++++++--- .../forms/hook_form_lib/hooks/use_form.ts | 18 ++++++++++++++++-- .../static/forms/hook_form_lib/types.ts | 2 ++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index d55ef5d19c39c..20651df52e6f2 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -36,9 +36,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) deserializer = (value: unknown) => value, } = config; - const [value, setStateValue] = useState( - typeof defaultValue === 'function' ? deserializer(defaultValue()) : deserializer(defaultValue) - ); + const initialValue = + typeof defaultValue === 'function' ? deserializer(defaultValue()) : deserializer(defaultValue); + + const [value, setStateValue] = useState(initialValue); const [errors, setErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); @@ -358,6 +359,15 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) return errorMessages ? errorMessages : null; }; + const reset = () => { + setValue(initialValue); + setErrors([]); + setPristine(true); + setValidating(false); + setIsChangingValue(false); + setIsValidated(false); + }; + const serializeOutput: FieldHook['__serializeOutput'] = (rawValue = value) => serializer(rawValue); @@ -396,6 +406,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) setErrors: _setErrors, clearErrors, validate, + reset, __serializeOutput: serializeOutput, }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 9b048849069db..5e6df7cee6549 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -47,7 +47,7 @@ export function useForm( const formOptions = { ...DEFAULT_OPTIONS, ...options }; const defaultValueDeserialized = Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue); - const [isSubmitted, setSubmitted] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); const [isValid, setIsValid] = useState(undefined); const fieldsRefs = useRef({}); @@ -202,7 +202,7 @@ export function useForm( } if (!isSubmitted) { - setSubmitted(true); // User has attempted to submit the form at least once + setIsSubmitted(true); // User has attempted to submit the form at least once } setSubmitting(true); @@ -230,6 +230,19 @@ export function useForm( return subscription; }; + /** + * Reset all the fields of the form to their default values + * and reset all the states to their original value. + */ + const reset: FormHook['reset'] = () => { + Object.entries(fieldsRefs.current).forEach(([path, field]) => { + field.reset(); + }); + setIsSubmitted(false); + setSubmitting(false); + setIsValid(undefined); + }; + const form: FormHook = { isSubmitted, isSubmitting, @@ -241,6 +254,7 @@ export function useForm( getFields, getFormData, getFieldDefaultValue, + reset, __options: formOptions, __formData$: formData$, __updateFormDataAt: updateFormDataAt, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 3449951877702..495ab04c0167d 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -35,6 +35,7 @@ export interface FormHook { getFields: () => FieldsMap; getFormData: (options?: { unflatten?: boolean }) => T; getFieldDefaultValue: (fieldName: string) => unknown; + reset: () => void; readonly __options: Required; readonly __formData$: MutableRefObject>; __addField: (field: FieldHook) => void; @@ -106,6 +107,7 @@ export interface FieldHook { value?: unknown; validationType?: string; }) => FieldValidateResponse | Promise; + reset: () => void; __serializeOutput: (rawValue?: unknown) => unknown; } From 99ebcf5c0ddf33d81fc912d89d989b37d96a1d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 15:48:24 +0200 Subject: [PATCH 49/64] Leave component after adding a prop --- .../properties/create_property.tsx | 4 +++- .../ui/components/mappings_editor/lib/utils.ts | 16 ++++++++++------ .../ui/components/mappings_editor/reducer.ts | 16 ++++++++++------ .../ui/components/mappings_editor/types.ts | 1 + 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx index 6b0f6be386825..deb812a87a040 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx @@ -16,15 +16,17 @@ import { } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; +import { Property } from '../../../types'; export const CreateProperty = () => { - const { form } = useForm(); + const { form } = useForm(); const dispatch = useDispatch(); const saveProperty = async () => { const { isValid, data } = await form.submit(); if (isValid) { dispatch({ type: 'property.add', value: data }); + form.reset(); } }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 8850dc9f1a697..281c2b2333e6a 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -26,7 +26,11 @@ const getChildPropertiesName = (dataType: DataType): ChildPropertyName | undefin return undefined; }; -const getPropertyMeta = (property: Property, propertyPath: string): PropertyMeta => { +export const getPropertyMeta = ( + property: Property, + path: string, + parentPath?: string +): PropertyMeta => { const childPropertiesName = getChildPropertiesName(property.type); const canHaveChildProperties = Boolean(childPropertiesName); const hasChildProperties = @@ -35,13 +39,12 @@ const getPropertyMeta = (property: Property, propertyPath: string): PropertyMeta Object.keys(property[childPropertiesName]!).length > 0; const childProperties = hasChildProperties - ? Object.keys(property[childPropertiesName!]!).map( - propertyName => `${propertyPath}.${propertyName}` - ) + ? Object.keys(property[childPropertiesName!]!).map(propertyName => `${path}.${propertyName}`) : undefined; return { - path: propertyPath, + path, + parentPath, hasChildProperties, childPropertiesName, canHaveChildProperties, @@ -82,10 +85,11 @@ export const normalize = (propertiesToNormalize: Properties): NormalizedProperti paths: string[] = [] ): Record => Object.entries(props).reduce((acc, [propName, value]) => { + const parentPath = paths.length ? paths.join('.') : undefined; const propertyPathArray = [...paths, propName]; const propertyPath = propertyPathArray.join('.'); const property = { name: propName, ...value } as Property; - const meta = getPropertyMeta(property, propertyPath); + const meta = getPropertyMeta(property, propertyPath, parentPath); const normalizedProperty: NormalizedProperty = { resource: property, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 221b2b86e8a61..626c13a541aec 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; -import { NormalizedProperties } from './types'; +import { Property, NormalizedProperties } from './types'; +import { getPropertyMeta } from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -36,7 +37,7 @@ export interface State { export type Action = | { type: 'configuration.update'; value: OnFormUpdateArg } - | { type: 'property.add'; value: any } + | { type: 'property.add'; value: Property } | { type: 'property.remove'; value: any } | { type: 'property.edit'; value: any } | { type: 'documentField.createProperty'; value?: string } @@ -82,13 +83,16 @@ export const reducer = (state: State, action: Action): State => { const { fieldPathToAddProperty } = state.documentFields; const { name } = action.value; const addToRootLevel = fieldPathToAddProperty === undefined; - const propId = addToRootLevel ? name : `${fieldPathToAddProperty}.${name}`; + const propertyPath = addToRootLevel ? name : `${fieldPathToAddProperty}.${name}`; const rootLevelFields = addToRootLevel ? [...state.properties.rootLevelFields, name] : state.properties.rootLevelFields; - state.properties.byId[propId] = action.value; + state.properties.byId[propertyPath] = { + resource: action.value, + ...getPropertyMeta(action.value, propertyPath, fieldPathToAddProperty), + }; if (!addToRootLevel) { const parentProperty = state.properties.byId[fieldPathToAddProperty!]; @@ -98,14 +102,14 @@ export const reducer = (state: State, action: Action): State => { // Update parent property with new children state.properties.byId[fieldPathToAddProperty!] = { ...parentProperty, - childProperties: [propId, ...childProperties], + childProperties: [propertyPath, ...childProperties], + hasChildProperties: true, }; } return { ...state, properties: { ...state.properties, rootLevelFields }, - documentFields: { ...state.documentFields, status: 'idle' }, }; } case 'property.edit': { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index 64741a2e10313..e9fed2cbda896 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -96,6 +96,7 @@ export interface Property { export interface PropertyMeta { path: string; + parentPath: string | undefined; childPropertiesName: ChildPropertyName | undefined; canHaveChildProperties: boolean; hasChildProperties: boolean; From 183c6be133a3266e5a801ffd8fa2fe97f7e29bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 16:00:41 +0200 Subject: [PATCH 50/64] Allow create property by hitting ENTER key --- .../properties/create_property.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx index deb812a87a040..f92cbf88c08f4 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx @@ -18,11 +18,16 @@ import { FIELD_TYPES_OPTIONS } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; import { Property } from '../../../types'; +const formWrapper = (props: any) =>
            ; + export const CreateProperty = () => { const { form } = useForm(); const dispatch = useDispatch(); - const saveProperty = async () => { + const submitForm = async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } const { isValid, data } = await form.submit(); if (isValid) { dispatch({ type: 'property.add', value: data }); @@ -35,7 +40,7 @@ export const CreateProperty = () => { }; return ( - + { /> - Save + + Save + - Cancel + Done From a48b481b858fb7897269e3686ecae7a3175460d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 16:21:09 +0200 Subject: [PATCH 51/64] Denormalize properties --- .../components/mappings_editor/lib/utils.ts | 19 +++++++++++++++++++ .../mappings_editor/mappings_state.tsx | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 281c2b2333e6a..22921b4065e80 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -113,3 +113,22 @@ export const normalize = (propertiesToNormalize: Properties): NormalizedProperti rootLevelFields: Object.keys(propertiesToNormalize), }; }; + +export const deNormalize = (normalized: NormalizedProperties): Properties => { + const deNormalizePaths = (paths: string[], to: Properties = {}) => { + paths.forEach(path => { + const { resource, childProperties, childPropertiesName } = normalized.byId[path]; + const { name, ...property } = resource; + to[name] = property; + if (childProperties) { + if (!property[childPropertiesName!]) { + property[childPropertiesName!] = {}; + } + return deNormalizePaths(childProperties, property[childPropertiesName!]); + } + }); + return to; + }; + + return deNormalizePaths(normalized.rootLevelFields); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index e25597a370151..64b4b735b259d 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -8,7 +8,7 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; import { reducer, MappingsConfiguration, MappingsProperties, State, Dispatch } from './reducer'; import { Property } from './types'; -import { normalize } from './lib'; +import { normalize, deNormalize } from './lib'; type Mappings = MappingsConfiguration & { properties: MappingsProperties; @@ -64,7 +64,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P onUpdate({ getData: () => ({ ...state.configuration.data.format(), - properties: state.properties.byId, + properties: deNormalize(state.properties), }), validate: () => { return state.configuration.validate(); From ed118621a32a220f700e80ea6f49433e5a97756c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 16:58:39 +0200 Subject: [PATCH 52/64] Remove test validation --- .../components/configuration_form/form.schema.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts index 96b7161e625d9..2c61524365411 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/configuration_form/form.schema.ts @@ -34,15 +34,6 @@ export const schema: FormSchema = { type: FIELD_TYPES.COMBO_BOX, defaultValue: [], validations: [ - { - validator: ({ value }) => { - if ((value as string[]).length === 0) { - return { - message: 'Add at least one', - }; - } - }, - }, { validator: containsCharsField({ message: 'Spaces are not allowed.', From a8554681b77194d0dc462d39b6ede482fa3a1d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 3 Oct 2019 17:13:08 +0200 Subject: [PATCH 53/64] Update reducer to remove a property --- .../properties/properties_list_item.tsx | 5 ++++- .../ui/components/mappings_editor/reducer.ts | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx index 4d14935845b19..f8542a215a866 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx @@ -46,7 +46,10 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop }; const removeField = () => { - // console.log('Removing', propertyPath); + dispatch({ + type: 'property.remove', + value: path, + }); }; const renderCreateProperty = () => { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 626c13a541aec..55666fff15c10 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -112,11 +112,26 @@ export const reducer = (state: State, action: Action): State => { properties: { ...state.properties, rootLevelFields }, }; } + case 'property.remove': { + const { parentPath, path } = state.properties.byId[action.value]; + if (parentPath) { + // Deleting a child property + const parentProperty = state.properties.byId[parentPath]; + parentProperty.childProperties = parentProperty.childProperties!.filter( + childPath => childPath !== path + ); + } else { + // Deleting a root level field + state.properties.rootLevelFields = state.properties.rootLevelFields.filter( + fieldPath => fieldPath !== path + ); + } + + delete state.properties.byId[path]; + } case 'property.edit': { - // const properties = state.properties.data; // Todo update this to merge new prop return { ...state, - // properties: { ...state.properties, data: properties }, documentFields: { ...state.documentFields, status: 'idle' }, }; } From 2302d9476f2ceb94fa2a8ce0128c18c4e4d3d9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 4 Oct 2019 10:43:34 +0200 Subject: [PATCH 54/64] Refactor: "properties" --> "fields" --- .../document_fields/document_fields.tsx | 34 +++-- .../create_field.tsx} | 12 +- .../document_fields/fields/edit_field.tsx | 103 +++++++++++++ .../document_fields/fields/fields_list.tsx | 27 ++++ .../fields_list_item.tsx} | 51 ++++--- .../{properties => fields}/index.ts | 8 +- .../properties/properties_list.tsx | 27 ---- .../components/mappings_editor/lib/utils.ts | 140 ++++++++++++------ .../mappings_editor/mappings_editor.tsx | 4 +- .../mappings_editor/mappings_state.tsx | 16 +- .../ui/components/mappings_editor/reducer.ts | 93 ++++++------ .../ui/components/mappings_editor/types.ts | 32 ++-- 12 files changed, 349 insertions(+), 198 deletions(-) rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/{properties/create_property.tsx => fields/create_field.tsx} (88%) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/{properties/properties_list_item.tsx => fields/fields_list_item.tsx} (52%) rename x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/{properties => fields}/index.ts (65%) delete mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index 31a1d27ff2fab..a578d2c1c8287 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -9,33 +9,33 @@ import { EuiButton, EuiSpacer } from '@elastic/eui'; import { useState, useDispatch } from '../../mappings_state'; import { DocumentFieldsHeaders } from './document_fields_header'; -import { PropertiesList, CreateProperty } from './properties'; +import { FieldsList, CreateField, EditField } from './fields'; export const DocumentFields = () => { const dispatch = useDispatch(); const { - properties: { byId, rootLevelFields }, - documentFields: { status, fieldPathToAddProperty }, + fields: { byId, rootLevelFields }, + documentFields: { status, fieldPathToAddField }, } = useState(); - const getProperty = (propId: string) => byId[propId]; - const properties = rootLevelFields.map(getProperty); + const getField = (propId: string) => byId[propId]; + const fields = rootLevelFields.map(getField); const addField = () => { - dispatch({ type: 'documentField.createProperty' }); + dispatch({ type: 'documentField.createField' }); }; - const renderCreateProperty = () => { - // Root level (0) does not have the "fieldPathToAddProperty" set - if (status !== 'creatingProperty' || fieldPathToAddProperty !== undefined) { + const renderCreateField = () => { + // Root level (0) does not have the "fieldPathToAddField" set + if (status !== 'creatingField' || fieldPathToAddField !== undefined) { return null; } - return ; + return ; }; const renderAddFieldButton = () => { - if (status === 'creatingProperty') { + if (status !== 'idle') { return null; } return ( @@ -46,12 +46,20 @@ export const DocumentFields = () => { ); }; + const renderEditField = () => { + if (status !== 'editingField') { + return null; + } + return ; + }; + return ( <> - - {renderCreateProperty()} + + {renderCreateField()} {renderAddFieldButton()} + {renderEditField()} ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx similarity index 88% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx index f92cbf88c08f4..85415a9489037 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/create_property.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -16,12 +16,12 @@ import { } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; -import { Property } from '../../../types'; +import { Field } from '../../../types'; const formWrapper = (props: any) =>
            ; -export const CreateProperty = () => { - const { form } = useForm(); +export const CreateField = () => { + const { form } = useForm(); const dispatch = useDispatch(); const submitForm = async (e?: React.FormEvent) => { @@ -30,7 +30,7 @@ export const CreateProperty = () => { } const { isValid, data } = await form.submit(); if (isValid) { - dispatch({ type: 'property.add', value: data }); + dispatch({ type: 'field.add', value: data }); form.reset(); } }; @@ -63,11 +63,11 @@ export const CreateProperty = () => { - Save + Add - Done + Cancel diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx new file mode 100644 index 0000000000000..e2647050402c2 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx @@ -0,0 +1,103 @@ +/* + * 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 { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + // EuiButton, + // EuiFlexGroup, + // EuiFlexItem, +} from '@elastic/eui'; + +// import { +// useForm, +// Form, +// TextField, +// SelectField, +// UseField, +// fieldValidators, +// } from '../../../shared_imports'; +// import { FIELD_TYPES_OPTIONS } from '../../../constants'; +// import { useDispatch } from '../../../mappings_state'; +import { Field } from '../../../types'; + +// const formWrapper = (props: any) =>
            ; + +interface Props { + field?: Field; +} + +export const EditField = ({ field }: Props) => { + // const { form } = useForm({ defaultValue: field }); + // const dispatch = useDispatch(); + + // const submitForm = async (e?: React.FormEvent) => { + // if (e) { + // e.preventDefault(); + // } + // const { isValid, data } = await form.submit(); + // if (isValid) { + // dispatch({ type: 'field.add', value: data }); + // form.reset(); + // } + // }; + + // const cancel = () => { + // dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + // }; + + return ( + undefined} + aria-labelledby="autoFollowPatternDetailsFlyoutTitle" + size="m" + maxWidth={400} + > + + +

            Edit field

            +
            +
            + Content of the flyout +
            + ); +}; + +/* + + + + + + + + + + Add + + + + Done + + + */ diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx new file mode 100644 index 0000000000000..f08610accf61f --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -0,0 +1,27 @@ +/* + * 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 { FieldsListItem } from './fields_list_item'; +import { NormalizedField } from '../../../types'; + +interface Props { + fields?: NormalizedField[]; + path?: string; + treeDepth?: number; +} + +export const FieldsList = React.memo(({ fields = [], treeDepth = 0, path = '' }: Props) => { + return ( +
              + {fields.map(field => ( +
            • + +
            • + ))} +
            + ); +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx similarity index 52% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index f8542a215a866..3d6c2264c1d7f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; -import { PropertiesList } from './properties_list'; -import { CreateProperty } from './create_property'; -import { NormalizedProperty } from '../../../types'; +import { FieldsList } from './fields_list'; +import { CreateField } from './create_field'; +import { NormalizedField } from '../../../types'; interface Props { - property: NormalizedProperty; + field: NormalizedField; parentPath: string; treeDepth?: number; } @@ -25,46 +25,49 @@ const inlineStyle = { alignItems: 'center', }; -export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Props) => { +export const FieldsListItem = ({ field, parentPath, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { - documentFields: { status, fieldPathToAddProperty }, - properties: { byId }, + documentFields: { status, fieldPathToAddField }, + fields: { byId }, } = useState(); - const getProperty = (propId: string) => byId[propId]; - const { path, resource, childProperties, hasChildProperties, canHaveChildProperties } = property; + const getField = (propId: string) => byId[propId]; + const { path, source, childFields, hasChildFields, canHaveChildFields } = field; const addField = () => { dispatch({ - type: 'documentField.createProperty', + type: 'documentField.createField', value: path, }); }; const editField = () => { - // console.log('Editing', propertyPath); + dispatch({ + type: 'documentField.editField', + value: path, + }); }; const removeField = () => { dispatch({ - type: 'property.remove', + type: 'field.remove', value: path, }); }; - const renderCreateProperty = () => { - if (status !== 'creatingProperty') { + const renderCreateField = () => { + if (status !== 'creatingField') { return null; } - // Root level (0) has does not have the "fieldPathToAddProperty" set - if (fieldPathToAddProperty !== path) { + // Root level (0) has does not have the "fieldPathToAddField" set + if (fieldPathToAddField !== path) { return null; } return (
            - +
            ); }; @@ -77,7 +80,7 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop return ( <> Edit - {canHaveChildProperties && Add field} + {canHaveChildFields && Add field} Remove ); @@ -86,18 +89,14 @@ export const PropertiesListItem = ({ property, parentPath, treeDepth = 0 }: Prop return ( <>
            - {resource.name} | {resource.type} {renderActionButtons()} + {source.name} | {source.type} {renderActionButtons()}
            - {renderCreateProperty()} + {renderCreateField()} - {hasChildProperties && ( + {hasChildFields && (
            - +
            )} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts similarity index 65% rename from x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts rename to x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts index 75e6dac5de4ad..8ec007b0df384 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './properties_list'; +export * from './fields_list'; -export * from './properties_list_item'; +export * from './fields_list_item'; -export * from './create_property'; +export * from './create_field'; + +export * from './edit_field'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx deleted file mode 100644 index 4f9ba04896441..0000000000000 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/properties/properties_list.tsx +++ /dev/null @@ -1,27 +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 React from 'react'; - -import { PropertiesListItem } from './properties_list_item'; -import { NormalizedProperty } from '../../../types'; - -interface Props { - properties?: NormalizedProperty[]; - path?: string; - treeDepth?: number; -} - -export const PropertiesList = React.memo(({ properties = [], treeDepth = 0, path = '' }: Props) => { - return ( -
              - {properties.map(property => ( -
            • - -
            • - ))} -
            - ); -}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 22921b4065e80..cf9036aebf264 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -6,18 +6,18 @@ import { DataType, - Properties, - Property, - NormalizedProperties, - NormalizedProperty, - PropertyMeta, + Fields, + Field, + NormalizedFields, + NormalizedField, + FieldMeta, MainType, SubType, - ChildPropertyName, + ChildFieldName, } from '../types'; import { DATA_TYPE_DEFINITION } from '../constants'; -const getChildPropertiesName = (dataType: DataType): ChildPropertyName | undefined => { +const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { if (dataType === 'text' || dataType === 'keyword') { return 'fields'; } else if (dataType === 'object' || dataType === 'nested') { @@ -26,29 +26,25 @@ const getChildPropertiesName = (dataType: DataType): ChildPropertyName | undefin return undefined; }; -export const getPropertyMeta = ( - property: Property, - path: string, - parentPath?: string -): PropertyMeta => { - const childPropertiesName = getChildPropertiesName(property.type); - const canHaveChildProperties = Boolean(childPropertiesName); - const hasChildProperties = - childPropertiesName !== undefined && - Boolean(property[childPropertiesName]) && - Object.keys(property[childPropertiesName]!).length > 0; - - const childProperties = hasChildProperties - ? Object.keys(property[childPropertiesName!]!).map(propertyName => `${path}.${propertyName}`) +export const getFieldMeta = (field: Field, path: string, parentPath?: string): FieldMeta => { + const childFieldsName = getChildFieldsName(field.type); + const canHaveChildFields = Boolean(childFieldsName); + const hasChildFields = + childFieldsName !== undefined && + Boolean(field[childFieldsName]) && + Object.keys(field[childFieldsName]!).length > 0; + + const childFields = hasChildFields + ? Object.keys(field[childFieldsName!]!).map(propertyName => `${path}.${propertyName}`) : undefined; return { path, parentPath, - hasChildProperties, - childPropertiesName, - canHaveChildProperties, - childProperties, + hasChildFields, + childFieldsName, + canHaveChildFields, + childFields, }; }; @@ -78,53 +74,99 @@ const subTypesMapToType = Object.entries(DATA_TYPE_DEFINITION).reduce( export const getTypeFromSubType = (subType: SubType): MainType => subTypesMapToType[subType] as MainType; -export const normalize = (propertiesToNormalize: Properties): NormalizedProperties => { - const normalizeProperties = ( - props: Properties, - to: NormalizedProperties['byId'] = {}, +/** + * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields + * to a `byId` object where the key is the path to the field and the value is a `NormalizedField`. + * The `NormalizedField` contains the field data under `source` and meta information about the capability of the field. + * + * @example + +// original +{ + myObject: { + type: 'object', + properties: { + name: { + type: 'text' + } + } + } +} + +// normalized +{ + rootLevelFields: ['myObject'], + byId: { + myObject: { + source: { type: 'object' }, + path: 'myObject', + parentPath: undefined, + hasChildFields: true, + childFieldsName: 'properties', // "object" type have their child fields under "properties" + canHaveChildFields: true, + childFields: ['myObject.name'], + }, + 'myObject.name': { + source: { type: 'text' }, + path: 'myObject.name', + parentPath: 'myObject', + hasChildFields: false, + childFieldsName: 'fields', // "text" type have their child fields under "fields" + canHaveChildFields: true, + childFields: undefined, + }, + }, +} + * + * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types) + */ +export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { + const normalizeFields = ( + props: Fields, + to: NormalizedFields['byId'] = {}, paths: string[] = [] ): Record => Object.entries(props).reduce((acc, [propName, value]) => { const parentPath = paths.length ? paths.join('.') : undefined; const propertyPathArray = [...paths, propName]; const propertyPath = propertyPathArray.join('.'); - const property = { name: propName, ...value } as Property; - const meta = getPropertyMeta(property, propertyPath, parentPath); + const field = { name: propName, ...value } as Field; + const meta = getFieldMeta(field, propertyPath, parentPath); + const { properties, fields, ...rest } = field; - const normalizedProperty: NormalizedProperty = { - resource: property, + const normalizedField: NormalizedField = { + source: rest, ...meta, }; - acc[propertyPath] = normalizedProperty; + acc[propertyPath] = normalizedField; - if (meta.hasChildProperties) { - return normalizeProperties(property[meta.childPropertiesName!]!, to, propertyPathArray); + if (meta.hasChildFields) { + return normalizeFields(field[meta.childFieldsName!]!, to, propertyPathArray); } - acc[propertyPath] = normalizedProperty; + acc[propertyPath] = normalizedField; return acc; }, to); - const byId = normalizeProperties(propertiesToNormalize); + const byId = normalizeFields(fieldsToNormalize); return { byId, - rootLevelFields: Object.keys(propertiesToNormalize), + rootLevelFields: Object.keys(fieldsToNormalize), }; }; -export const deNormalize = (normalized: NormalizedProperties): Properties => { - const deNormalizePaths = (paths: string[], to: Properties = {}) => { +export const deNormalize = (normalized: NormalizedFields): Fields => { + const deNormalizePaths = (paths: string[], to: Fields = {}) => { paths.forEach(path => { - const { resource, childProperties, childPropertiesName } = normalized.byId[path]; - const { name, ...property } = resource; - to[name] = property; - if (childProperties) { - if (!property[childPropertiesName!]) { - property[childPropertiesName!] = {}; - } - return deNormalizePaths(childProperties, property[childPropertiesName!]); + const { source, childFields, childFieldsName } = normalized.byId[path]; + const { name, ...normalizedField } = source; + const field: Omit = normalizedField; + to[name] = field; + if (childFields) { + field[childFieldsName!] = {}; + return deNormalizePaths(childFields, field[childFieldsName!]); } }); return to; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 115c0edf70102..47e0619bbe41e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -25,10 +25,10 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props }), {} as Types['MappingsConfiguration'] ); - const propertiesDefaultValue = defaultValue.properties || {}; + const fieldsDefaultValue = defaultValue.properties || {}; return ( - + diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index 64b4b735b259d..dc79b917f822f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -6,18 +6,18 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; -import { reducer, MappingsConfiguration, MappingsProperties, State, Dispatch } from './reducer'; -import { Property } from './types'; +import { reducer, MappingsConfiguration, MappingsFields, State, Dispatch } from './reducer'; +import { Field } from './types'; import { normalize, deNormalize } from './lib'; type Mappings = MappingsConfiguration & { - properties: MappingsProperties; + properties: MappingsFields; }; export interface Types { Mappings: Mappings; MappingsConfiguration: MappingsConfiguration; - MappingsProperties: MappingsProperties; + MappingsFields: MappingsFields; } export interface OnUpdateHandlerArg { @@ -33,12 +33,12 @@ const DispatchContext = createContext(undefined); export interface Props { children: React.ReactNode; - defaultValue: { properties: { [key: string]: Property } }; + defaultValue: { fields: { [key: string]: Field } }; onUpdate: OnUpdateHandler; } export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { - const { byId, rootLevelFields } = normalize(defaultValue.properties); + const { byId, rootLevelFields } = normalize(defaultValue.fields); const initialState: State = { isValid: undefined, configuration: { @@ -48,7 +48,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, validate: () => Promise.resolve(false), }, - properties: { + fields: { byId, rootLevelFields, isValid: true, @@ -64,7 +64,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P onUpdate({ getData: () => ({ ...state.configuration.data.format(), - properties: deNormalize(state.properties), + properties: deNormalize(state.fields), }), validate: () => { return state.configuration.validate(); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 55666fff15c10..0c296df55ebf0 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; -import { Property, NormalizedProperties } from './types'; -import { getPropertyMeta } from './lib'; +import { Field, NormalizedFields } from './types'; +import { getFieldMeta } from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -14,11 +14,11 @@ export interface MappingsConfiguration { dynamic_date_formats: string[]; } -export interface MappingsProperties { +export interface MappingsFields { [key: string]: any; } -type DocumentFieldsStatus = 'idle' | 'editingProperty' | 'creatingProperty'; +type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField'; export interface State { isValid: boolean | undefined; @@ -26,22 +26,22 @@ export interface State { documentFields: { status: DocumentFieldsStatus; propertyToEdit?: string; - fieldPathToAddProperty?: string; + fieldPathToAddField?: string; }; - properties: { - byId: NormalizedProperties['byId']; - rootLevelFields: NormalizedProperties['rootLevelFields']; + fields: { + byId: NormalizedFields['byId']; + rootLevelFields: NormalizedFields['rootLevelFields']; isValid: boolean | undefined; }; } export type Action = | { type: 'configuration.update'; value: OnFormUpdateArg } - | { type: 'property.add'; value: Property } - | { type: 'property.remove'; value: any } - | { type: 'property.edit'; value: any } - | { type: 'documentField.createProperty'; value?: string } - | { type: 'documentField.editProperty'; value: string } + | { type: 'field.add'; value: Field } + | { type: 'field.remove'; value: any } + | { type: 'field.edit'; value: any } + | { type: 'documentField.createField'; value?: string } + | { type: 'documentField.editField'; value: string } | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }; export type Dispatch = (action: Action) => void; @@ -50,86 +50,83 @@ export const reducer = (state: State, action: Action): State => { switch (action.type) { case 'configuration.update': const isValid = - action.value.isValid === undefined || state.properties.isValid === undefined + action.value.isValid === undefined || state.fields.isValid === undefined ? undefined - : action.value.isValid && state.properties.isValid; + : action.value.isValid && state.fields.isValid; return { ...state, isValid, configuration: action.value, }; - case 'documentField.createProperty': + case 'documentField.createField': return { ...state, documentFields: { ...state.documentFields, - fieldPathToAddProperty: action.value, - status: 'creatingProperty', + fieldPathToAddField: action.value, + status: 'creatingField', }, }; - case 'documentField.editProperty': + case 'documentField.editField': return { ...state, documentFields: { ...state.documentFields, - status: 'editingProperty', + status: 'editingField', propertyToEdit: action.value, }, }; case 'documentField.changeStatus': return { ...state, documentFields: { ...state.documentFields, status: action.value } }; - case 'property.add': { - const { fieldPathToAddProperty } = state.documentFields; + case 'field.add': { + const { fieldPathToAddField } = state.documentFields; const { name } = action.value; - const addToRootLevel = fieldPathToAddProperty === undefined; - const propertyPath = addToRootLevel ? name : `${fieldPathToAddProperty}.${name}`; + const addToRootLevel = fieldPathToAddField === undefined; + const propertyPath = addToRootLevel ? name : `${fieldPathToAddField}.${name}`; const rootLevelFields = addToRootLevel - ? [...state.properties.rootLevelFields, name] - : state.properties.rootLevelFields; + ? [...state.fields.rootLevelFields, name] + : state.fields.rootLevelFields; - state.properties.byId[propertyPath] = { - resource: action.value, - ...getPropertyMeta(action.value, propertyPath, fieldPathToAddProperty), + state.fields.byId[propertyPath] = { + source: action.value, + ...getFieldMeta(action.value, propertyPath, fieldPathToAddField), }; if (!addToRootLevel) { - const parentProperty = state.properties.byId[fieldPathToAddProperty!]; - const childProperties = parentProperty.childProperties || []; - // TODO HERE: create a new Set() intead of an empty array? + const parentField = state.fields.byId[fieldPathToAddField!]; + const childFields = parentField.childFields || []; - // Update parent property with new children - state.properties.byId[fieldPathToAddProperty!] = { - ...parentProperty, - childProperties: [propertyPath, ...childProperties], - hasChildProperties: true, + // Update parent field with new children + state.fields.byId[fieldPathToAddField!] = { + ...parentField, + childFields: [propertyPath, ...childFields], + hasChildFields: true, }; } return { ...state, - properties: { ...state.properties, rootLevelFields }, + fields: { ...state.fields, rootLevelFields }, }; } - case 'property.remove': { - const { parentPath, path } = state.properties.byId[action.value]; + case 'field.remove': { + const { parentPath, path } = state.fields.byId[action.value]; if (parentPath) { - // Deleting a child property - const parentProperty = state.properties.byId[parentPath]; - parentProperty.childProperties = parentProperty.childProperties!.filter( - childPath => childPath !== path - ); + // Deleting a child field + const parentField = state.fields.byId[parentPath]; + parentField.childFields = parentField.childFields!.filter(childPath => childPath !== path); } else { // Deleting a root level field - state.properties.rootLevelFields = state.properties.rootLevelFields.filter( + state.fields.rootLevelFields = state.fields.rootLevelFields.filter( fieldPath => fieldPath !== path ); } - delete state.properties.byId[path]; + delete state.fields.byId[path]; } - case 'property.edit': { + case 'field.edit': { return { ...state, documentFields: { ...state.documentFields, status: 'idle' }, diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index e9fed2cbda896..46034b5041617 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -82,36 +82,36 @@ export type ParameterName = | 'ignore_above' | 'split_queries_on_whitespace'; -export interface Properties { - [key: string]: Omit; +export interface Fields { + [key: string]: Omit; } -export interface Property { +export interface Field { name: string; type: DataType; - properties?: { [key: string]: Property }; - fields?: { [key: string]: Property }; - [key: string]: any; + properties?: { [key: string]: Field }; + fields?: { [key: string]: Field }; + // [key: string]: any; } -export interface PropertyMeta { +export interface FieldMeta { path: string; parentPath: string | undefined; - childPropertiesName: ChildPropertyName | undefined; - canHaveChildProperties: boolean; - hasChildProperties: boolean; - childProperties: string[] | undefined; + childFieldsName: ChildFieldName | undefined; + canHaveChildFields: boolean; + hasChildFields: boolean; + childFields: string[] | undefined; } -export interface NormalizedProperties { +export interface NormalizedFields { byId: { - [id: string]: NormalizedProperty; + [id: string]: NormalizedField; }; rootLevelFields: string[]; } -export interface NormalizedProperty extends PropertyMeta { - resource: Property; +export interface NormalizedField extends FieldMeta { + source: Omit; } -export type ChildPropertyName = 'properties' | 'fields'; +export type ChildFieldName = 'properties' | 'fields'; From 6bb5baab28bc20970b5b1f1ebe91de9c579ad8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 4 Oct 2019 12:20:20 +0200 Subject: [PATCH 55/64] Edit field basic --- .../document_fields/document_fields.tsx | 5 +- .../document_fields/fields/edit_field.tsx | 107 ++++++++++-------- .../components/mappings_editor/lib/utils.ts | 2 +- .../ui/components/mappings_editor/reducer.ts | 63 +++++++++-- 4 files changed, 115 insertions(+), 62 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index a578d2c1c8287..3f279645dcb4e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -15,7 +15,7 @@ export const DocumentFields = () => { const dispatch = useDispatch(); const { fields: { byId, rootLevelFields }, - documentFields: { status, fieldPathToAddField }, + documentFields: { status, fieldPathToAddField, fieldToEdit }, } = useState(); const getField = (propId: string) => byId[propId]; @@ -50,7 +50,8 @@ export const DocumentFields = () => { if (status !== 'editingField') { return null; } - return ; + const field = byId[fieldToEdit!].source; + return ; }; return ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx index e2647050402c2..0d98fadf70b80 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx @@ -9,68 +9,54 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiTitle, - // EuiButton, - // EuiFlexGroup, - // EuiFlexItem, + EuiButton, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; -// import { -// useForm, -// Form, -// TextField, -// SelectField, -// UseField, -// fieldValidators, -// } from '../../../shared_imports'; -// import { FIELD_TYPES_OPTIONS } from '../../../constants'; -// import { useDispatch } from '../../../mappings_state'; +import { + useForm, + Form, + TextField, + SelectField, + UseField, + fieldValidators, +} from '../../../shared_imports'; +import { FIELD_TYPES_OPTIONS } from '../../../constants'; +import { useDispatch } from '../../../mappings_state'; import { Field } from '../../../types'; -// const formWrapper = (props: any) =>
            ; +const formWrapper = (props: any) => ; interface Props { - field?: Field; + field: Field; } export const EditField = ({ field }: Props) => { - // const { form } = useForm({ defaultValue: field }); - // const dispatch = useDispatch(); + const { form } = useForm({ defaultValue: field }); + const dispatch = useDispatch(); - // const submitForm = async (e?: React.FormEvent) => { - // if (e) { - // e.preventDefault(); - // } - // const { isValid, data } = await form.submit(); - // if (isValid) { - // dispatch({ type: 'field.add', value: data }); - // form.reset(); - // } - // }; + const exitEdit = () => { + dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + }; - // const cancel = () => { - // dispatch({ type: 'documentField.changeStatus', value: 'idle' }); - // }; + const submitForm = async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + const { isValid, data } = await form.submit(); + if (isValid) { + dispatch({ type: 'field.edit', value: data }); + exitEdit(); + } + }; - return ( - undefined} - aria-labelledby="autoFollowPatternDetailsFlyoutTitle" - size="m" - maxWidth={400} - > - - -

            Edit field

            -
            -
            - Content of the flyout -
            - ); -}; + const cancel = () => { + exitEdit(); + }; -/* + const renderTempForm = () => ( + { }} /> + + Add @@ -100,4 +88,23 @@ export const EditField = ({ field }: Props) => { Done - */ + + ); + + return ( + + + +

            Edit field

            +
            +
            + {renderTempForm()} +
            + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index cf9036aebf264..31d30ddcd097e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -76,7 +76,7 @@ export const getTypeFromSubType = (subType: SubType): MainType => /** * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields - * to a `byId` object where the key is the path to the field and the value is a `NormalizedField`. + * to a `byId` object where the key is the **path** to the field and the value is a `NormalizedField`. * The `NormalizedField` contains the field data under `source` and meta information about the capability of the field. * * @example diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 0c296df55ebf0..5011e77695edb 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; -import { Field, NormalizedFields } from './types'; +import { Field, NormalizedFields, NormalizedField } from './types'; import { getFieldMeta } from './lib'; export interface MappingsConfiguration { @@ -25,7 +25,7 @@ export interface State { configuration: OnFormUpdateArg; documentFields: { status: DocumentFieldsStatus; - propertyToEdit?: string; + fieldToEdit?: string; fieldPathToAddField?: string; }; fields: { @@ -38,8 +38,8 @@ export interface State { export type Action = | { type: 'configuration.update'; value: OnFormUpdateArg } | { type: 'field.add'; value: Field } - | { type: 'field.remove'; value: any } - | { type: 'field.edit'; value: any } + | { type: 'field.remove'; value: string } + | { type: 'field.edit'; value: Field } | { type: 'documentField.createField'; value?: string } | { type: 'documentField.editField'; value: string } | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }; @@ -74,7 +74,7 @@ export const reducer = (state: State, action: Action): State => { documentFields: { ...state.documentFields, status: 'editingField', - propertyToEdit: action.value, + fieldToEdit: action.value, }, }; case 'documentField.changeStatus': @@ -83,15 +83,15 @@ export const reducer = (state: State, action: Action): State => { const { fieldPathToAddField } = state.documentFields; const { name } = action.value; const addToRootLevel = fieldPathToAddField === undefined; - const propertyPath = addToRootLevel ? name : `${fieldPathToAddField}.${name}`; + const fieldPath = addToRootLevel ? name : `${fieldPathToAddField}.${name}`; const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, name] : state.fields.rootLevelFields; - state.fields.byId[propertyPath] = { + state.fields.byId[fieldPath] = { source: action.value, - ...getFieldMeta(action.value, propertyPath, fieldPathToAddField), + ...getFieldMeta(action.value, fieldPath, fieldPathToAddField), }; if (!addToRootLevel) { @@ -101,7 +101,7 @@ export const reducer = (state: State, action: Action): State => { // Update parent field with new children state.fields.byId[fieldPathToAddField!] = { ...parentField, - childFields: [propertyPath, ...childFields], + childFields: [fieldPath, ...childFields], hasChildFields: true, }; } @@ -125,8 +125,53 @@ export const reducer = (state: State, action: Action): State => { } delete state.fields.byId[path]; + return state; } case 'field.edit': { + const fieldToEdit = state.documentFields.fieldToEdit!; + const previousField = state.fields.byId[fieldToEdit!]; + const { parentPath, source: previousFieldSource } = previousField; + + // As the "name" might have changed, we first calculate the field path that we are editing + const newFieldPath = parentPath ? `${parentPath}.${action.value.name}` : action.value.name; + + const newField: NormalizedField = { + source: action.value, + ...getFieldMeta(action.value, newFieldPath, parentPath), + }; + + // Check if the name has changed + if (newFieldPath !== fieldToEdit) { + // The name has changed... + if (parentPath) { + // ---> Update the parent `childFields` array + const parentField = state.fields.byId[parentPath]; + // swap old field path with new one + parentField.childFields = parentField.childFields!.map(path => + path === fieldToEdit ? newFieldPath : path + ); + } else { + // ---> Update the root level fields + state.fields.rootLevelFields = state.fields.rootLevelFields.map(path => + path === fieldToEdit ? newFieldPath : path + ); + } + + // We need to update all the ids of the child fields + + // Make sure to delete the old normalized field + // delete state.fields.byId[fieldToEdit]; + } + + // Check if the type has changed. If it has we need to delete + // recursively all child properties + if (newField.source.type !== previousFieldSource.type) { + // const allChildFields = getAllChildFields(fieldToEdit, state.fields.byId); + // debugger; + } + + state.fields.byId[newFieldPath] = newField; + return { ...state, documentFields: { ...state.documentFields, status: 'idle' }, From 5ed329187e5e2cd2fd590bcbe5b013e57be6e5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 4 Oct 2019 16:13:50 +0200 Subject: [PATCH 56/64] Use unique id for fields + core edit functionality --- .../document_fields/document_fields.tsx | 8 +- .../document_fields/fields/edit_field.tsx | 5 +- .../document_fields/fields/fields_list.tsx | 7 +- .../fields/fields_list_item.tsx | 19 ++-- .../components/mappings_editor/lib/utils.ts | 71 ++++++------ .../ui/components/mappings_editor/reducer.ts | 107 ++++++++---------- .../ui/components/mappings_editor/types.ts | 7 +- 7 files changed, 110 insertions(+), 114 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index 3f279645dcb4e..2a805f32a67be 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -15,10 +15,10 @@ export const DocumentFields = () => { const dispatch = useDispatch(); const { fields: { byId, rootLevelFields }, - documentFields: { status, fieldPathToAddField, fieldToEdit }, + documentFields: { status, fieldToAddFieldTo, fieldToEdit }, } = useState(); - const getField = (propId: string) => byId[propId]; + const getField = (fieldId: string) => byId[fieldId]; const fields = rootLevelFields.map(getField); const addField = () => { @@ -26,8 +26,8 @@ export const DocumentFields = () => { }; const renderCreateField = () => { - // Root level (0) does not have the "fieldPathToAddField" set - if (status !== 'creatingField' || fieldPathToAddField !== undefined) { + // Root level (0) does not have the "fieldToAddFieldTo" set + if (status !== 'creatingField' || fieldToAddFieldTo !== undefined) { return null; } diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx index 0d98fadf70b80..4a0ca0db93b38 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx @@ -68,7 +68,6 @@ export const EditField = ({ field }: Props) => { { - Add + Update - Done + Cancel diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx index f08610accf61f..fa505a47b0a8d 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -10,16 +10,15 @@ import { NormalizedField } from '../../../types'; interface Props { fields?: NormalizedField[]; - path?: string; treeDepth?: number; } -export const FieldsList = React.memo(({ fields = [], treeDepth = 0, path = '' }: Props) => { +export const FieldsList = React.memo(({ fields = [], treeDepth = 0 }: Props) => { return (
              {fields.map(field => ( -
            • - +
            • +
            • ))}
            diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 3d6c2264c1d7f..e87faa8cc6aa7 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -13,7 +13,6 @@ import { NormalizedField } from '../../../types'; interface Props { field: NormalizedField; - parentPath: string; treeDepth?: number; } @@ -25,33 +24,33 @@ const inlineStyle = { alignItems: 'center', }; -export const FieldsListItem = ({ field, parentPath, treeDepth = 0 }: Props) => { +export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { - documentFields: { status, fieldPathToAddField }, + documentFields: { status, fieldToAddFieldTo }, fields: { byId }, } = useState(); const getField = (propId: string) => byId[propId]; - const { path, source, childFields, hasChildFields, canHaveChildFields } = field; + const { id, source, childFields, hasChildFields, canHaveChildFields } = field; const addField = () => { dispatch({ type: 'documentField.createField', - value: path, + value: id, }); }; const editField = () => { dispatch({ type: 'documentField.editField', - value: path, + value: id, }); }; const removeField = () => { dispatch({ type: 'field.remove', - value: path, + value: id, }); }; @@ -60,8 +59,8 @@ export const FieldsListItem = ({ field, parentPath, treeDepth = 0 }: Props) => { return null; } - // Root level (0) has does not have the "fieldPathToAddField" set - if (fieldPathToAddField !== path) { + // Root level (0) has does not have the "fieldToAddFieldTo" set + if (fieldToAddFieldTo !== id) { return null; } @@ -96,7 +95,7 @@ export const FieldsListItem = ({ field, parentPath, treeDepth = 0 }: Props) => { {hasChildFields && (
            - +
            )} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 31d30ddcd097e..74304b313225f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -17,6 +17,15 @@ import { } from '../types'; import { DATA_TYPE_DEFINITION } from '../constants'; +export const getUniqueId = () => { + return ( + '_' + + (Number(String(Math.random()).slice(2)) + Date.now() + Math.round(performance.now())).toString( + 36 + ) + ); +}; + const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { if (dataType === 'text' || dataType === 'keyword') { return 'fields'; @@ -26,7 +35,7 @@ const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { return undefined; }; -export const getFieldMeta = (field: Field, path: string, parentPath?: string): FieldMeta => { +export const getFieldMeta = (field: Field): FieldMeta => { const childFieldsName = getChildFieldsName(field.type); const canHaveChildFields = Boolean(childFieldsName); const hasChildFields = @@ -34,17 +43,10 @@ export const getFieldMeta = (field: Field, path: string, parentPath?: string): F Boolean(field[childFieldsName]) && Object.keys(field[childFieldsName]!).length > 0; - const childFields = hasChildFields - ? Object.keys(field[childFieldsName!]!).map(propertyName => `${path}.${propertyName}`) - : undefined; - return { - path, - parentPath, hasChildFields, childFieldsName, canHaveChildFields, - childFields, }; }; @@ -95,21 +97,21 @@ export const getTypeFromSubType = (subType: SubType): MainType => // normalized { - rootLevelFields: ['myObject'], + rootLevelFields: ['_uniqueId123'], byId: { - myObject: { + '_uniqueId123': { source: { type: 'object' }, - path: 'myObject', - parentPath: undefined, + id: '_uniqueId123', + parentId: undefined, hasChildFields: true, childFieldsName: 'properties', // "object" type have their child fields under "properties" canHaveChildFields: true, - childFields: ['myObject.name'], + childFields: ['_uniqueId456'], }, - 'myObject.name': { + '_uniqueId456': { source: { type: 'text' }, - path: 'myObject.name', - parentPath: 'myObject', + id: '_uniqueId456', + parentId: '_uniqueId123', hasChildFields: false, childFieldsName: 'fields', // "text" type have their child fields under "fields" canHaveChildFields: true, @@ -124,43 +126,48 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { const normalizeFields = ( props: Fields, to: NormalizedFields['byId'] = {}, - paths: string[] = [] + idsArray: string[], + parentId?: string ): Record => Object.entries(props).reduce((acc, [propName, value]) => { - const parentPath = paths.length ? paths.join('.') : undefined; - const propertyPathArray = [...paths, propName]; - const propertyPath = propertyPathArray.join('.'); + const id = getUniqueId(); + idsArray.push(id); const field = { name: propName, ...value } as Field; - const meta = getFieldMeta(field, propertyPath, parentPath); + const meta = getFieldMeta(field); + const { childFieldsName } = meta; + + if (childFieldsName && field[childFieldsName]) { + meta.childFields = []; + normalizeFields(field[meta.childFieldsName!]!, to, meta.childFields, id); + } + const { properties, fields, ...rest } = field; const normalizedField: NormalizedField = { + id, + parentId, source: rest, ...meta, }; - acc[propertyPath] = normalizedField; - - if (meta.hasChildFields) { - return normalizeFields(field[meta.childFieldsName!]!, to, propertyPathArray); - } + acc[id] = normalizedField; - acc[propertyPath] = normalizedField; return acc; }, to); - const byId = normalizeFields(fieldsToNormalize); + const rootLevelFields: string[] = []; + const byId = normalizeFields(fieldsToNormalize, {}, rootLevelFields); return { byId, - rootLevelFields: Object.keys(fieldsToNormalize), + rootLevelFields, }; }; export const deNormalize = (normalized: NormalizedFields): Fields => { - const deNormalizePaths = (paths: string[], to: Fields = {}) => { - paths.forEach(path => { - const { source, childFields, childFieldsName } = normalized.byId[path]; + const deNormalizePaths = (ids: string[], to: Fields = {}) => { + ids.forEach(id => { + const { source, childFields, childFieldsName } = normalized.byId[id]; const { name, ...normalizedField } = source; const field: Omit = normalizedField; to[name] = field; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 5011e77695edb..1ac16d30542af 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -5,7 +5,7 @@ */ import { OnFormUpdateArg } from './shared_imports'; import { Field, NormalizedFields, NormalizedField } from './types'; -import { getFieldMeta } from './lib'; +import { getFieldMeta, getUniqueId } from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -26,7 +26,7 @@ export interface State { documentFields: { status: DocumentFieldsStatus; fieldToEdit?: string; - fieldPathToAddField?: string; + fieldToAddFieldTo?: string; }; fields: { byId: NormalizedFields['byId']; @@ -64,7 +64,7 @@ export const reducer = (state: State, action: Action): State => { ...state, documentFields: { ...state.documentFields, - fieldPathToAddField: action.value, + fieldToAddFieldTo: action.value, status: 'creatingField', }, }; @@ -80,28 +80,29 @@ export const reducer = (state: State, action: Action): State => { case 'documentField.changeStatus': return { ...state, documentFields: { ...state.documentFields, status: action.value } }; case 'field.add': { - const { fieldPathToAddField } = state.documentFields; - const { name } = action.value; - const addToRootLevel = fieldPathToAddField === undefined; - const fieldPath = addToRootLevel ? name : `${fieldPathToAddField}.${name}`; + const id = getUniqueId(); + const { fieldToAddFieldTo } = state.documentFields; + const addToRootLevel = fieldToAddFieldTo === undefined; const rootLevelFields = addToRootLevel - ? [...state.fields.rootLevelFields, name] + ? [...state.fields.rootLevelFields, id] : state.fields.rootLevelFields; - state.fields.byId[fieldPath] = { + state.fields.byId[id] = { + id, + parentId: fieldToAddFieldTo, source: action.value, - ...getFieldMeta(action.value, fieldPath, fieldPathToAddField), + ...getFieldMeta(action.value), }; if (!addToRootLevel) { - const parentField = state.fields.byId[fieldPathToAddField!]; + const parentField = state.fields.byId[fieldToAddFieldTo!]; const childFields = parentField.childFields || []; // Update parent field with new children - state.fields.byId[fieldPathToAddField!] = { + state.fields.byId[fieldToAddFieldTo!] = { ...parentField, - childFields: [fieldPath, ...childFields], + childFields: [id, ...childFields], hasChildFields: true, }; } @@ -112,69 +113,61 @@ export const reducer = (state: State, action: Action): State => { }; } case 'field.remove': { - const { parentPath, path } = state.fields.byId[action.value]; - if (parentPath) { + const { id, parentId } = state.fields.byId[action.value]; + let { rootLevelFields } = state.fields; + if (parentId) { // Deleting a child field - const parentField = state.fields.byId[parentPath]; - parentField.childFields = parentField.childFields!.filter(childPath => childPath !== path); + const parentField = state.fields.byId[parentId]; + parentField.childFields = parentField.childFields!.filter(childId => childId !== id); } else { // Deleting a root level field - state.fields.rootLevelFields = state.fields.rootLevelFields.filter( - fieldPath => fieldPath !== path - ); + rootLevelFields = rootLevelFields.filter(childId => childId !== id); } - delete state.fields.byId[path]; - return state; + delete state.fields.byId[id]; + return { + ...state, + fields: { + ...state.fields, + rootLevelFields, + }, + }; } case 'field.edit': { const fieldToEdit = state.documentFields.fieldToEdit!; const previousField = state.fields.byId[fieldToEdit!]; - const { parentPath, source: previousFieldSource } = previousField; - - // As the "name" might have changed, we first calculate the field path that we are editing - const newFieldPath = parentPath ? `${parentPath}.${action.value.name}` : action.value.name; - const newField: NormalizedField = { + let newField: NormalizedField = { + ...previousField, source: action.value, - ...getFieldMeta(action.value, newFieldPath, parentPath), }; - // Check if the name has changed - if (newFieldPath !== fieldToEdit) { - // The name has changed... - if (parentPath) { - // ---> Update the parent `childFields` array - const parentField = state.fields.byId[parentPath]; - // swap old field path with new one - parentField.childFields = parentField.childFields!.map(path => - path === fieldToEdit ? newFieldPath : path - ); - } else { - // ---> Update the root level fields - state.fields.rootLevelFields = state.fields.rootLevelFields.map(path => - path === fieldToEdit ? newFieldPath : path - ); - } - - // We need to update all the ids of the child fields + if (newField.source.type !== previousField.source.type) { + // The field `type` has changed, we need to update its meta information + // and delete all its children fields. - // Make sure to delete the old normalized field - // delete state.fields.byId[fieldToEdit]; - } + newField = { + ...newField, + ...getFieldMeta(action.value), + childFields: undefined, + }; - // Check if the type has changed. If it has we need to delete - // recursively all child properties - if (newField.source.type !== previousFieldSource.type) { - // const allChildFields = getAllChildFields(fieldToEdit, state.fields.byId); - // debugger; + if (previousField.childFields) { + previousField.childFields.forEach(fieldId => { + delete state.fields.byId[fieldId]; + }); + } } - state.fields.byId[newFieldPath] = newField; - return { ...state, - documentFields: { ...state.documentFields, status: 'idle' }, + fields: { + ...state.fields, + byId: { + ...state.fields.byId, + [fieldToEdit]: newField, + }, + }, }; } default: diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index 46034b5041617..1451ac8778d45 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -91,16 +91,13 @@ export interface Field { type: DataType; properties?: { [key: string]: Field }; fields?: { [key: string]: Field }; - // [key: string]: any; } export interface FieldMeta { - path: string; - parentPath: string | undefined; childFieldsName: ChildFieldName | undefined; canHaveChildFields: boolean; hasChildFields: boolean; - childFields: string[] | undefined; + childFields?: string[]; } export interface NormalizedFields { @@ -111,6 +108,8 @@ export interface NormalizedFields { } export interface NormalizedField extends FieldMeta { + id: string; + parentId?: string; source: Omit; } From d53d722dd12cc279be6a245851ad6b43f6928390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 4 Oct 2019 17:18:09 +0200 Subject: [PATCH 57/64] Add name collision validation --- .../document_fields/document_fields.tsx | 2 +- .../document_fields/fields/create_field.tsx | 28 +++++++++++++---- .../document_fields/fields/edit_field.tsx | 31 +++++++++++++------ .../components/mappings_editor/lib/index.ts | 2 ++ .../mappings_editor/lib/validators.ts | 30 ++++++++++++++++++ .../mappings_editor/shared_imports.ts | 1 + 6 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index 2a805f32a67be..cec77035cd46d 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -50,7 +50,7 @@ export const DocumentFields = () => { if (status !== 'editingField') { return null; } - const field = byId[fieldToEdit!].source; + const field = byId[fieldToEdit!]; return ; }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx index 85415a9489037..9df45b486e65b 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -15,13 +15,18 @@ import { fieldValidators, } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS } from '../../../constants'; -import { useDispatch } from '../../../mappings_state'; +import { useState, useDispatch } from '../../../mappings_state'; import { Field } from '../../../types'; +import { validateUniqueName } from '../../../lib'; const formWrapper = (props: any) =>
            ; export const CreateField = () => { const { form } = useForm(); + const { + documentFields: { fieldToAddFieldTo }, + fields, + } = useState(); const dispatch = useDispatch(); const submitForm = async (e?: React.FormEvent) => { @@ -39,15 +44,26 @@ export const CreateField = () => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; + const nameValidations = [ + { + validator: fieldValidators.emptyField('Cannot be empty'), + }, + { + validator: fieldValidators.containsCharsField({ + chars: '.', + message: 'Cannot contain a dot (.)', + }), + }, + { + validator: validateUniqueName(fields, undefined, fieldToAddFieldTo), + }, + ]; + return ( - + ; interface Props { - field: Field; + field: NormalizedField; } export const EditField = ({ field }: Props) => { - const { form } = useForm({ defaultValue: field }); + const { form } = useForm({ defaultValue: field.source }); + const { fields } = useState(); const dispatch = useDispatch(); const exitEdit = () => { @@ -55,15 +57,26 @@ export const EditField = ({ field }: Props) => { exitEdit(); }; + const nameValidations = [ + { + validator: fieldValidators.emptyField('Cannot be empty'), + }, + { + validator: fieldValidators.containsCharsField({ + chars: '.', + message: 'Cannot contain a dot (.)', + }), + }, + { + validator: validateUniqueName(fields, field.source.name, field.parentId), + }, + ]; + const renderTempForm = () => ( - + { + const validator: ValidationFunc = ({ value }) => { + const existingNames = parentId + ? Object.values(byId) + .filter(field => field.parentId === parentId) + .map(field => field.source.name) + : rootLevelFields.map(fieldId => byId[fieldId].source.name); + + if (existingNames.filter(name => name !== initialName).includes(value as string)) { + return { + message: 'There is already a field with this name', + }; + } + }; + + return validator; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts index 5ea8f0952222b..e4a2ef6b43e51 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts @@ -13,6 +13,7 @@ export { FIELD_TYPES, VALIDATION_TYPES, OnFormUpdateArg, + ValidationFunc, } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { From 4e5e37ee0c33134ffa9a16ae659c10b540f2ebad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Sat, 5 Oct 2019 20:23:20 +0200 Subject: [PATCH 58/64] Add confirm modal when deleting a field with children --- .../document_fields/fields/create_field.tsx | 37 +- .../fields/delete_field_provider.tsx | 90 +++++ .../document_fields/fields/edit_field.tsx | 105 +++--- .../fields/fields_list_item.tsx | 12 +- .../document_fields/fields/index.ts | 4 + .../fields/update_field_provider.tsx | 120 ++++++ .../mappings_editor/constants/index.ts | 2 + .../constants/parameters_definition.ts | 355 ++++++++++++++++++ .../components/mappings_editor/lib/utils.ts | 17 + .../mappings_editor/lib/validators.ts | 2 +- .../ui/components/mappings_editor/reducer.ts | 27 +- .../mappings_editor/shared_imports.ts | 2 + .../ui/components/mappings_editor/types.ts | 7 + 13 files changed, 698 insertions(+), 82 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx index 9df45b486e65b..95ff3a8b0054f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -12,15 +12,18 @@ import { TextField, SelectField, UseField, - fieldValidators, + FieldConfig, } from '../../../shared_imports'; -import { FIELD_TYPES_OPTIONS } from '../../../constants'; +import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; import { useState, useDispatch } from '../../../mappings_state'; -import { Field } from '../../../types'; +import { Field, ParameterName } from '../../../types'; import { validateUniqueName } from '../../../lib'; const formWrapper = (props: any) => ; +const getFieldConfig = (param: ParameterName): FieldConfig => + PARAMETERS_DEFINITION[param].fieldConfig || {}; + export const CreateField = () => { const { form } = useForm(); const { @@ -44,31 +47,27 @@ export const CreateField = () => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; - const nameValidations = [ - { - validator: fieldValidators.emptyField('Cannot be empty'), - }, - { - validator: fieldValidators.containsCharsField({ - chars: '.', - message: 'Cannot contain a dot (.)', - }), - }, - { - validator: validateUniqueName(fields, undefined, fieldToAddFieldTo), - }, - ]; + const { validations, ...rest } = getFieldConfig('name'); + const nameConfig: FieldConfig = { + ...rest, + validations: [ + ...validations!, + { + validator: validateUniqueName(fields, undefined, fieldToAddFieldTo), + }, + ], + }; return ( - + void; + +interface Props { + children: (deleteProperty: DeleteFieldFunc) => React.ReactNode; +} + +interface State { + isModalOpen: boolean; + field: NormalizedField | undefined; +} + +export const DeleteFieldProvider = ({ children }: Props) => { + const [state, setState] = useState({ isModalOpen: false, field: undefined }); + const dispatch = useDispatch(); + const { + fields: { byId }, + } = useMappingsState(); + + const closeModal = () => { + setState({ isModalOpen: false, field: undefined }); + }; + + const deleteField: DeleteFieldFunc = field => { + const { hasChildFields } = field; + + if (hasChildFields) { + setState({ isModalOpen: true, field }); + } else { + dispatch({ type: 'field.remove', value: field.id }); + } + }; + + const confirmDelete = () => { + dispatch({ type: 'field.remove', value: state.field!.id }); + closeModal(); + }; + + const renderModal = () => { + const field = state.field!; + const childFields = field.childFields!.map(childId => byId[childId]); + const title = `Remove property '${field.source.name}'?`; + + return ( + + + +

            + This will also delete the following child fields and the possible child fields under + them. +

            +
              + {childFields + .map(_field => _field.source.name) + .sort() + .map(name => ( +
            • {name}
            • + ))} +
            +
            +
            +
            + ); + }; + + return ( + + {children(deleteField)} + {state.isModalOpen && renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx index 3e6a9506b04bc..8b295a3e69651 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx @@ -20,15 +20,19 @@ import { TextField, SelectField, UseField, - fieldValidators, + FieldConfig, } from '../../../shared_imports'; -import { FIELD_TYPES_OPTIONS } from '../../../constants'; +import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; import { useState, useDispatch } from '../../../mappings_state'; -import { Field, NormalizedField } from '../../../types'; +import { Field, NormalizedField, ParameterName } from '../../../types'; import { validateUniqueName } from '../../../lib'; +import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; const formWrapper = (props: any) => ; +const getFieldConfig = (param: ParameterName): FieldConfig => + PARAMETERS_DEFINITION[param].fieldConfig || {}; + interface Props { field: NormalizedField; } @@ -42,14 +46,13 @@ export const EditField = ({ field }: Props) => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; - const submitForm = async (e?: React.FormEvent) => { + const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { if (e) { e.preventDefault(); } const { isValid, data } = await form.submit(); if (isValid) { - dispatch({ type: 'field.edit', value: data }); - exitEdit(); + updateField({ ...field, source: data }); } }; @@ -57,50 +60,56 @@ export const EditField = ({ field }: Props) => { exitEdit(); }; - const nameValidations = [ - { - validator: fieldValidators.emptyField('Cannot be empty'), - }, - { - validator: fieldValidators.containsCharsField({ - chars: '.', - message: 'Cannot contain a dot (.)', - }), - }, - { - validator: validateUniqueName(fields, field.source.name, field.parentId), - }, - ]; + const { validations, ...rest } = getFieldConfig('name'); + const nameConfig: FieldConfig = { + ...rest, + validations: [ + ...validations!, + { + validator: validateUniqueName(fields, field.source.name, field.parentId), + }, + ], + }; const renderTempForm = () => ( - - - - - - - - - - - - - Update - - - - Cancel - - - + + {updateField => ( +
            + + + + + + + + + + + + Update + + + + Cancel + + +
            + )} +
            ); return ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index e87faa8cc6aa7..a5877b09ba5bc 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -9,6 +9,7 @@ import { EuiButton } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; +import { DeleteFieldProvider } from './delete_field_provider'; import { NormalizedField } from '../../../types'; interface Props { @@ -47,13 +48,6 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { }); }; - const removeField = () => { - dispatch({ - type: 'field.remove', - value: id, - }); - }; - const renderCreateField = () => { if (status !== 'creatingField') { return null; @@ -80,7 +74,9 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { <> Edit {canHaveChildFields && Add field} - Remove + + {deleteField => deleteField(field)}>Remove} + ); }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts index 8ec007b0df384..0d994d503c66c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/index.ts @@ -11,3 +11,7 @@ export * from './fields_list_item'; export * from './create_field'; export * from './edit_field'; + +export * from './delete_field_provider'; + +export * from './update_field_provider'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx new file mode 100644 index 0000000000000..1245365a1965f --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx @@ -0,0 +1,120 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +import { useState as useMappingsState, useDispatch } from '../../../mappings_state'; +import { shouldDeleteChildFieldsAfterTypeChange } from '../../../lib'; +import { NormalizedField, DataType } from '../../../types'; + +export type UpdateFieldFunc = (field: NormalizedField) => void; + +interface Props { + children: (saveProperty: UpdateFieldFunc) => React.ReactNode; +} + +interface State { + isModalOpen: boolean; + field?: NormalizedField; +} + +export const UpdateFieldProvider = ({ children }: Props) => { + const [state, setState] = useState({ + isModalOpen: false, + }); + const dispatch = useDispatch(); + + const { + fields: { byId }, + } = useMappingsState(); + + const closeModal = () => { + setState({ isModalOpen: false }); + }; + + const updateField: UpdateFieldFunc = field => { + const previousField = byId[field.id]; + + const handleTypeChange = ( + oldType: DataType, + newType: DataType + ): { requiresConfirmation: boolean } => { + const { hasChildFields } = field; + + if (!hasChildFields) { + // No child fields will be deleted, no confirmation needed. + return { requiresConfirmation: false }; + } + + const requiresConfirmation = shouldDeleteChildFieldsAfterTypeChange(oldType, newType); + + return { requiresConfirmation }; + }; + + if (field.source.type !== previousField.source.type) { + // We need to check if, by changing the type, we need + // to delete the possible child properties ("fields" or "properties") + // and warn the user about it. + const { requiresConfirmation } = handleTypeChange( + previousField.source.type, + field.source.type + ); + + if (requiresConfirmation) { + setState({ isModalOpen: true, field }); + return; + } + } + + dispatch({ type: 'field.edit', value: field.source }); + }; + + const confirmTypeUpdate = () => { + dispatch({ type: 'field.edit', value: state.field!.source }); + closeModal(); + }; + + const renderModal = () => { + const field = state.field!; + const title = `Confirm change '${field.source.name}' type to "${field.source.type}".`; + const childFields = field.childFields!.map(childId => byId[childId]); + + return ( + + + +

            + This will delete the following child fields and the possible child fields under them. +

            +
              + {childFields + .map(_field => _field.source.name) + .sort() + .map(name => ( +
            • {name}
            • + ))} +
            +
            +
            +
            + ); + }; + + return ( + + {children(updateField)} + {state.isModalOpen && renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts index 07d95ae11ec81..4ee65039d77ad 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts @@ -7,3 +7,5 @@ export * from './field_options'; export * from './data_types_definition'; + +export * from './parameters_definition'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts new file mode 100644 index 0000000000000..38ee66c0ba088 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts @@ -0,0 +1,355 @@ +/* + * 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 { FIELD_TYPES, fieldValidators, fieldFormatters } from '../shared_imports'; +import { ParameterName, Parameter } from '../types'; + +const { toInt } = fieldFormatters; +const { emptyField, containsCharsField } = fieldValidators; + +interface ValidatorArg { + value: T; + path: string; + formData: { [key: string]: any }; +} + +export const PARAMETERS_DEFINITION: { + [key in ParameterName]: Parameter; +} = { + name: { + fieldConfig: { + label: 'Field name', + defaultValue: '', + validations: [ + { + validator: emptyField('Give a name to the field.'), + }, + { + validator: containsCharsField({ + chars: ' ', + message: 'Spaces are not allowed in the name.', + }), + }, + { + validator: fieldValidators.containsCharsField({ + chars: '.', + message: 'Cannot contain a dot (.)', + }), + }, + ], + }, + }, + type: { + fieldConfig: { + label: 'Field type', + defaultValue: 'text', + type: FIELD_TYPES.SELECT, + }, + }, + store: { + fieldConfig: { + label: 'Store', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html', + }, + index: { + fieldConfig: { + label: 'Index', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + doc_values: { + fieldConfig: { + label: 'Doc values', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', + }, + fielddata: { + fieldConfig: { + label: 'Fielddata', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + docs: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html', + }, + coerce: { + fieldConfig: { + label: 'Coerce', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + ignore_malformed: { + fieldConfig: { + label: 'Ignore malformed', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + null_value: { + fieldConfig: { + label: 'Null value', + defaultValue: '', + type: FIELD_TYPES.TEXT, + }, + }, + boost: { + fieldConfig: { + label: 'Boost', + defaultValue: 1.0, + type: FIELD_TYPES.NUMBER, + formatters: [toInt], + validations: [ + { + validator: ({ value }: ValidatorArg) => { + if (value < 0) { + return { + message: 'The value must be greater or equal than 0.', + }; + } + }, + }, + ], + }, + }, + dynamic: { + fieldConfig: { + label: 'Dynamic', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + enabled: { + fieldConfig: { + label: 'Enabled', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + locale: { + fieldConfig: { + label: 'Locale', + defaultValue: '', + }, + }, + format: { + fieldConfig: { + label: 'Formats', + type: FIELD_TYPES.COMBO_BOX, + defaultValue: [], + serializer: (options: any[]): string | undefined => + options.length ? options.join('||') : undefined, + deSerializer: (formats?: string | any[]): any[] => + Array.isArray(formats) ? formats : (formats as string).split('||'), + }, + }, + analyzer: { + fieldConfig: { + label: 'Analyzer', + defaultValue: 'index_default', + type: FIELD_TYPES.SELECT, + }, + }, + search_analyzer: { + fieldConfig: { + label: 'Search analyzer', + defaultValue: 'index_default', + type: FIELD_TYPES.SELECT, + }, + }, + search_quote_analyzer: { + fieldConfig: { + label: 'Search quote analyzer', + defaultValue: 'index_default', + type: FIELD_TYPES.SELECT, + }, + }, + normalizer: { + fieldConfig: { + label: 'Normalizer', + defaultValue: '', + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: containsCharsField({ + chars: ' ', + message: 'Spaces are not allowed.', + }), + }, + ], + }, + }, + index_options: { + fieldConfig: { + label: 'Index options', + defaultValue: 'docs', + type: FIELD_TYPES.SELECT, + }, + }, + eager_global_ordinals: { + fieldConfig: { + label: 'Eager global ordinals', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + index_phrases: { + fieldConfig: { + label: 'Index phrases', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + norms: { + fieldConfig: { + label: 'Norms', + type: FIELD_TYPES.CHECKBOX, + defaultValue: true, + }, + }, + term_vector: { + fieldConfig: { + label: 'Term vectors', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + position_increment_gap: { + fieldConfig: { + label: 'Position increment gap', + type: FIELD_TYPES.NUMBER, + defaultValue: 100, + formatters: [toInt], + validations: [ + { + validator: emptyField('Set a position increment gap value.'), + }, + { + validator: ({ value }: ValidatorArg) => { + if (value < 0) { + return { + message: 'The value must be greater or equal than 0.', + }; + } + }, + }, + ], + }, + }, + index_prefixes: { + fieldConfig: { + min_chars: { + type: FIELD_TYPES.NUMBER, + defaultValue: 2, + helpText: 'Min chars.', + formatters: [toInt], + validations: [ + { + validator: emptyField('Set a min value.'), + }, + { + validator: ({ value }: ValidatorArg) => { + if (value < 0) { + return { + message: 'The value must be greater or equal than zero.', + }; + } + }, + }, + { + validator: ({ value, path, formData }: ValidatorArg) => { + const maxPath = path.replace('.min', '.max'); + const maxValue: number | string = formData[maxPath]; + + if (maxValue === '') { + return; + } + + if (value >= maxValue) { + return { + message: 'The value must be smaller than the max value.', + }; + } + }, + }, + ], + }, + max_chars: { + type: FIELD_TYPES.NUMBER, + defaultValue: 5, + helpText: 'Max chars.', + formatters: [toInt], + validations: [ + { + validator: emptyField('Set a max value.'), + }, + { + validator: ({ value }: ValidatorArg) => { + if (value > 20) { + return { + message: 'The value must be smaller or equal than 20.', + }; + } + }, + }, + { + validator: ({ value, path, formData }: ValidatorArg) => { + const minPath = path.replace('.max', '.min'); + const minValue: number | string = formData[minPath]; + + if (minValue === '') { + return; + } + + if (value <= minValue) { + return { + message: 'The value must be greater than the min value.', + }; + } + }, + }, + ], + }, + }, + }, + similarity: { + fieldConfig: { + label: 'Similarity algorithm', + defaultValue: 'BM25', + type: FIELD_TYPES.SELECT, + }, + }, + split_queries_on_whitespace: { + fieldConfig: { + label: 'Split queries on whitespace', + type: FIELD_TYPES.CHECKBOX, + defaultValue: false, + }, + }, + ignore_above: { + fieldConfig: { + label: 'Ignore above', + defaultValue: 2147483647, + type: FIELD_TYPES.NUMBER, + formatters: [toInt], + validations: [ + { + validator: ({ value }: ValidatorArg) => { + if ((value as number) < 0) { + return { + message: 'The value must be greater or equal than 0.', + }; + } + }, + }, + ], + }, + }, +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 74304b313225f..404c887258508 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -181,3 +181,20 @@ export const deNormalize = (normalized: NormalizedFields): Fields => { return deNormalizePaths(normalized.rootLevelFields); }; + +export const shouldDeleteChildFieldsAfterTypeChange = ( + oldType: DataType, + newType: DataType +): boolean => { + if (oldType === 'text' && newType !== 'keyword') { + return true; + } else if (oldType === 'keyword' && newType !== 'text') { + return true; + } else if (oldType === 'object' && newType !== 'nested') { + return true; + } else if (oldType === 'nested' && newType !== 'object') { + return true; + } + + return false; +}; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts index a0dc0977bef94..2944258c15acc 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts @@ -21,7 +21,7 @@ export const validateUniqueName = ( if (existingNames.filter(name => name !== initialName).includes(value as string)) { return { - message: 'There is already a field with this name', + message: 'There is already a field with this name.', }; } }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 1ac16d30542af..660c8073acca3 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -5,7 +5,7 @@ */ import { OnFormUpdateArg } from './shared_imports'; import { Field, NormalizedFields, NormalizedField } from './types'; -import { getFieldMeta, getUniqueId } from './lib'; +import { getFieldMeta, getUniqueId, shouldDeleteChildFieldsAfterTypeChange } from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -119,6 +119,7 @@ export const reducer = (state: State, action: Action): State => { // Deleting a child field const parentField = state.fields.byId[parentId]; parentField.childFields = parentField.childFields!.filter(childId => childId !== id); + parentField.hasChildFields = Boolean(parentField.childFields.length); } else { // Deleting a root level field rootLevelFields = rootLevelFields.filter(childId => childId !== id); @@ -149,18 +150,32 @@ export const reducer = (state: State, action: Action): State => { newField = { ...newField, ...getFieldMeta(action.value), - childFields: undefined, + hasChildFields: previousField.hasChildFields, // we need to put that back from our previous field }; - if (previousField.childFields) { - previousField.childFields.forEach(fieldId => { - delete state.fields.byId[fieldId]; - }); + const shouldDeleteChildFields = shouldDeleteChildFieldsAfterTypeChange( + previousField.source.type, + newField.source.type + ); + + if (shouldDeleteChildFields) { + newField.childFields = undefined; + newField.hasChildFields = false; + + if (previousField.childFields) { + previousField.childFields.forEach(fieldId => { + delete state.fields.byId[fieldId]; + }); + } } } return { ...state, + documentFields: { + ...state.documentFields, + status: 'idle', + }, fields: { ...state.fields, byId: { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts index e4a2ef6b43e51..a816fea3b7678 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/shared_imports.ts @@ -14,6 +14,7 @@ export { VALIDATION_TYPES, OnFormUpdateArg, ValidationFunc, + FieldConfig, } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { @@ -25,4 +26,5 @@ export { export { fieldValidators, + fieldFormatters, } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index 1451ac8778d45..f2004dd096384 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -3,6 +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 { FieldConfig } from './shared_imports'; export interface DataTypeDefinition { label: string; @@ -82,6 +83,12 @@ export type ParameterName = | 'ignore_above' | 'split_queries_on_whitespace'; +export interface Parameter { + fieldConfig?: FieldConfig | { [key: string]: FieldConfig }; + paramName?: string; + docs?: string; +} + export interface Fields { [key: string]: Omit; } From 090c82aa378742215b2d27495d9b39de3a174662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 7 Oct 2019 12:00:46 +0200 Subject: [PATCH 59/64] Add max nested depth + invalid field form validation --- .../document_fields/document_fields.tsx | 21 +++- .../document_fields/fields/create_field.tsx | 32 ++++--- .../document_fields/fields/edit_field.tsx | 22 +++-- .../fields/fields_list_item.tsx | 42 ++++++-- .../mappings_editor/constants/index.ts | 2 + .../constants/mappings_editor.ts | 11 +++ .../components/mappings_editor/lib/utils.ts | 49 +++++++++- .../mappings_editor/lib/validators.ts | 2 +- .../mappings_editor/mappings_editor.tsx | 13 ++- .../mappings_editor/mappings_state.tsx | 28 ++++-- .../ui/components/mappings_editor/reducer.ts | 95 ++++++++++++++----- .../ui/components/mappings_editor/types.ts | 4 + 12 files changed, 252 insertions(+), 69 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/mappings_editor.ts diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx index cec77035cd46d..fdf0bad9cbf4c 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/document_fields.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiButton, EuiSpacer } from '@elastic/eui'; import { useState, useDispatch } from '../../mappings_state'; +import { validateUniqueName } from '../../lib'; import { DocumentFieldsHeaders } from './document_fields_header'; import { FieldsList, CreateField, EditField } from './fields'; @@ -21,17 +22,27 @@ export const DocumentFields = () => { const getField = (fieldId: string) => byId[fieldId]; const fields = rootLevelFields.map(getField); + const uniqueNameValidatorCreate = useMemo(() => { + return validateUniqueName({ rootLevelFields, byId }); + }, [byId, rootLevelFields]); + + const uniqueNameValidatorEdit = useMemo(() => { + if (fieldToEdit === undefined) { + return; + } + return validateUniqueName({ rootLevelFields, byId }, byId[fieldToEdit!].source.name); + }, [byId, rootLevelFields, fieldToEdit]); + const addField = () => { dispatch({ type: 'documentField.createField' }); }; const renderCreateField = () => { - // Root level (0) does not have the "fieldToAddFieldTo" set + // The "fieldToAddFieldTo" is undefined when adding to the top level "properties" object. if (status !== 'creatingField' || fieldToAddFieldTo !== undefined) { return null; } - - return ; + return ; }; const renderAddFieldButton = () => { @@ -51,7 +62,7 @@ export const DocumentFields = () => { return null; } const field = byId[fieldToEdit!]; - return ; + return ; }; return ( diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx index 95ff3a8b0054f..241f3522a1824 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -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 from 'react'; +import React, { useEffect } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { @@ -13,33 +13,43 @@ import { SelectField, UseField, FieldConfig, + ValidationFunc, } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; -import { useState, useDispatch } from '../../../mappings_state'; +import { useDispatch } from '../../../mappings_state'; import { Field, ParameterName } from '../../../types'; -import { validateUniqueName } from '../../../lib'; const formWrapper = (props: any) =>
            ; const getFieldConfig = (param: ParameterName): FieldConfig => PARAMETERS_DEFINITION[param].fieldConfig || {}; -export const CreateField = () => { +interface Props { + uniqueNameValidator: ValidationFunc; +} + +export const CreateField = React.memo(({ uniqueNameValidator }: Props) => { const { form } = useForm(); - const { - documentFields: { fieldToAddFieldTo }, - fields, - } = useState(); const dispatch = useDispatch(); + useEffect(() => { + const subscription = form.subscribe(updatedFieldForm => { + dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); + }); + + return subscription.unsubscribe; + }, [form]); + const submitForm = async (e?: React.FormEvent) => { if (e) { e.preventDefault(); } + const { isValid, data } = await form.submit(); + if (isValid) { - dispatch({ type: 'field.add', value: data }); form.reset(); + dispatch({ type: 'field.add', value: data }); } }; @@ -53,7 +63,7 @@ export const CreateField = () => { validations: [ ...validations!, { - validator: validateUniqueName(fields, undefined, fieldToAddFieldTo), + validator: uniqueNameValidator, }, ], }; @@ -87,4 +97,4 @@ export const CreateField = () => { ); -}; +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx index 8b295a3e69651..a7bdbefa45ffe 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/edit_field.tsx @@ -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 from 'react'; +import React, { useEffect } from 'react'; import { EuiFlyout, EuiFlyoutHeader, @@ -21,11 +21,11 @@ import { SelectField, UseField, FieldConfig, + ValidationFunc, } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; -import { useState, useDispatch } from '../../../mappings_state'; +import { useDispatch } from '../../../mappings_state'; import { Field, NormalizedField, ParameterName } from '../../../types'; -import { validateUniqueName } from '../../../lib'; import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; const formWrapper = (props: any) =>
            ; @@ -35,13 +35,21 @@ const getFieldConfig = (param: ParameterName): FieldConfig => interface Props { field: NormalizedField; + uniqueNameValidator: ValidationFunc; } -export const EditField = ({ field }: Props) => { +export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { const { form } = useForm({ defaultValue: field.source }); - const { fields } = useState(); const dispatch = useDispatch(); + useEffect(() => { + const subscription = form.subscribe(updatedFieldForm => { + dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); + }); + + return subscription.unsubscribe; + }, [form]); + const exitEdit = () => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; @@ -66,7 +74,7 @@ export const EditField = ({ field }: Props) => { validations: [ ...validations!, { - validator: validateUniqueName(fields, field.source.name, field.parentId), + validator: uniqueNameValidator, }, ], }; @@ -128,4 +136,4 @@ export const EditField = ({ field }: Props) => { {renderTempForm()} ); -}; +}); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index a5877b09ba5bc..d2bc8e4f84df1 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -3,14 +3,16 @@ * 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 { EuiButton } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; import { DeleteFieldProvider } from './delete_field_provider'; import { NormalizedField } from '../../../types'; +import { MAX_DEPTH_DEFAULT_EDITOR } from '../../../constants'; +import { validateUniqueName } from '../../../lib'; interface Props { field: NormalizedField; @@ -18,21 +20,24 @@ interface Props { } const inlineStyle = { - padding: '20px 0', borderBottom: '1px solid #ddd', - height: '82px', display: 'flex', - alignItems: 'center', + flexDirection: 'column' as 'column', }; export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { documentFields: { status, fieldToAddFieldTo }, - fields: { byId }, + fields: { byId, rootLevelFields }, } = useState(); const getField = (propId: string) => byId[propId]; const { id, source, childFields, hasChildFields, canHaveChildFields } = field; + const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; + + const uniqueNameValidator = useMemo(() => { + return validateUniqueName({ rootLevelFields, byId }, undefined, id); + }, [byId, rootLevelFields]); const addField = () => { dispatch({ @@ -60,7 +65,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return (
            - +
            ); }; @@ -73,7 +78,13 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( <> Edit - {canHaveChildFields && Add field} + {canHaveChildFields && ( + <> + + Add field + + + )} {deleteField => deleteField(field)}>Remove} @@ -84,7 +95,20 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( <>
            - {source.name} | {source.type} {renderActionButtons()} +
            + {source.name} | {source.type} {renderActionButtons()} +
            + {status === 'idle' && canHaveChildFields && isAddFieldBtnDisabled && ( +

            + You have reached the maximum depth for the mappings editor. Switch to the{' '} + dispatch({ type: 'documentField.changeEditor', value: 'json' })} + > + JSON editor + + to add more fields. +

            + )}
            {renderCreateField()} diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts index 4ee65039d77ad..5686e09e08b43 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/index.ts @@ -9,3 +9,5 @@ export * from './field_options'; export * from './data_types_definition'; export * from './parameters_definition'; + +export * from './mappings_editor'; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/mappings_editor.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/mappings_editor.ts new file mode 100644 index 0000000000000..fbb2b18eeb16d --- /dev/null +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/mappings_editor.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * The max nested depth allowed for child fields. + * Above this thresold, the user has to use the JSON editor. + */ +export const MAX_DEPTH_DEFAULT_EDITOR = 4; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts index 404c887258508..8962cd25af74e 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/utils.ts @@ -123,10 +123,13 @@ export const getTypeFromSubType = (subType: SubType): MainType => * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types) */ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { + let maxNestedDepth = 0; + const normalizeFields = ( props: Fields, to: NormalizedFields['byId'] = {}, idsArray: string[], + nestedDepth: number, parentId?: string ): Record => Object.entries(props).reduce((acc, [propName, value]) => { @@ -138,7 +141,8 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { if (childFieldsName && field[childFieldsName]) { meta.childFields = []; - normalizeFields(field[meta.childFieldsName!]!, to, meta.childFields, id); + maxNestedDepth = Math.max(maxNestedDepth, nestedDepth + 1); + normalizeFields(field[meta.childFieldsName!]!, to, meta.childFields, nestedDepth + 1, id); } const { properties, fields, ...rest } = field; @@ -146,6 +150,7 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { const normalizedField: NormalizedField = { id, parentId, + nestedDepth, source: rest, ...meta, }; @@ -156,11 +161,12 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { }, to); const rootLevelFields: string[] = []; - const byId = normalizeFields(fieldsToNormalize, {}, rootLevelFields); + const byId = normalizeFields(fieldsToNormalize, {}, rootLevelFields, 0); return { byId, rootLevelFields, + maxNestedDepth, }; }; @@ -182,6 +188,45 @@ export const deNormalize = (normalized: NormalizedFields): Fields => { return deNormalizePaths(normalized.rootLevelFields); }; +/** + * Retrieve recursively all the children fields of a field + * + * @param field The field to return the children from + * @param byId Map of all the document fields + */ +export const getAllChildFields = ( + field: NormalizedField, + byId: NormalizedFields['byId'] +): NormalizedField[] => { + const getChildFields = (_field: NormalizedField, to: NormalizedField[] = []) => { + if (_field.hasChildFields) { + _field + .childFields!.map(fieldId => byId[fieldId]) + .forEach(childField => { + to.push(childField); + getChildFields(childField, to); + }); + } + return to; + }; + + return getChildFields(field); +}; + +/** + * Return the max nested depth of the document fields + * + * @param byId Map of all the document fields + */ +export const getMaxNestedDepth = (byId: NormalizedFields['byId']): number => + Object.values(byId).reduce((maxDepth, field) => { + return Math.max(maxDepth, field.nestedDepth); + }, 0); + +/** + * When changing the type of a field, in most cases we want to delete all its child fields. + * There are some exceptions, when changing from "text" to "keyword" as both have the same "fields" property. + */ export const shouldDeleteChildFieldsAfterTypeChange = ( oldType: DataType, newType: DataType diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts index 2944258c15acc..98f34da40c245 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/lib/validators.ts @@ -8,7 +8,7 @@ import { ValidationFunc } from '../shared_imports'; import { NormalizedFields } from '../types'; export const validateUniqueName = ( - { rootLevelFields, byId }: NormalizedFields, + { rootLevelFields, byId }: Pick, initialName: string | undefined = '', parentId?: string ) => { diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx index 47e0619bbe41e..f53a5f1a430e8 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_editor.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { ConfigurationForm, CONFIGURATION_FIELDS, DocumentFields } from './components'; - import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state'; interface Props { @@ -27,10 +26,18 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props ); const fieldsDefaultValue = defaultValue.properties || {}; + const renderJsonEditor = () => { + return

            JSON editor

            ; + }; + return ( - - + {fieldsEditor => ( + <> + + {fieldsEditor === 'json' ? renderJsonEditor() : } + + )} ); }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx index dc79b917f822f..7a17da4c39f4f 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/mappings_state.tsx @@ -7,7 +7,8 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; import { reducer, MappingsConfiguration, MappingsFields, State, Dispatch } from './reducer'; -import { Field } from './types'; +import { MAX_DEPTH_DEFAULT_EDITOR } from './constants'; +import { Field, FieldsEditor } from './types'; import { normalize, deNormalize } from './lib'; type Mappings = MappingsConfiguration & { @@ -32,13 +33,14 @@ const StateContext = createContext(undefined); const DispatchContext = createContext(undefined); export interface Props { - children: React.ReactNode; + children: (editor: FieldsEditor) => React.ReactNode; defaultValue: { fields: { [key: string]: Field } }; onUpdate: OnUpdateHandler; } export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: Props) => { - const { byId, rootLevelFields } = normalize(defaultValue.fields); + const { byId, rootLevelFields, maxNestedDepth } = normalize(defaultValue.fields); + const initialState: State = { isValid: undefined, configuration: { @@ -46,19 +48,21 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P raw: {}, format: () => ({} as Mappings), }, - validate: () => Promise.resolve(false), + validate: () => Promise.resolve(true), }, fields: { byId, rootLevelFields, - isValid: true, + maxNestedDepth, }, documentFields: { status: 'idle', + editor: maxNestedDepth >= MAX_DEPTH_DEFAULT_EDITOR ? 'json' : 'default', }, }; const [state, dispatch] = useReducer(reducer, initialState); + useEffect(() => { // console.log('State update', state); onUpdate({ @@ -66,8 +70,14 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P ...state.configuration.data.format(), properties: deNormalize(state.fields), }), - validate: () => { - return state.configuration.validate(); + validate: async () => { + if (state.fieldForm === undefined) { + return await state.configuration.validate(); + } + + return Promise.all([state.configuration.validate(), state.fieldForm.validate()]).then( + ([isConfigurationValid, isFormFieldValid]) => isConfigurationValid && isFormFieldValid + ); }, isValid: state.isValid, }); @@ -75,7 +85,9 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P return ( - {children} + + {children(state.documentFields.editor)} + ); }); diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 660c8073acca3..c31b2ad9947cf 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { OnFormUpdateArg } from './shared_imports'; -import { Field, NormalizedFields, NormalizedField } from './types'; -import { getFieldMeta, getUniqueId, shouldDeleteChildFieldsAfterTypeChange } from './lib'; +import { Field, NormalizedFields, NormalizedField, FieldsEditor } from './types'; +import { + getFieldMeta, + getUniqueId, + shouldDeleteChildFieldsAfterTypeChange, + getAllChildFields, + getMaxNestedDepth, +} from './lib'; export interface MappingsConfiguration { dynamic: boolean | string; @@ -20,45 +26,61 @@ export interface MappingsFields { type DocumentFieldsStatus = 'idle' | 'editingField' | 'creatingField'; +interface DocumentFieldsState { + status: DocumentFieldsStatus; + editor: FieldsEditor; + fieldToEdit?: string; + fieldToAddFieldTo?: string; +} + export interface State { isValid: boolean | undefined; configuration: OnFormUpdateArg; - documentFields: { - status: DocumentFieldsStatus; - fieldToEdit?: string; - fieldToAddFieldTo?: string; - }; - fields: { - byId: NormalizedFields['byId']; - rootLevelFields: NormalizedFields['rootLevelFields']; - isValid: boolean | undefined; - }; + documentFields: DocumentFieldsState; + fields: NormalizedFields; + fieldForm?: OnFormUpdateArg; } export type Action = | { type: 'configuration.update'; value: OnFormUpdateArg } + | { type: 'fieldForm.update'; value: OnFormUpdateArg } | { type: 'field.add'; value: Field } | { type: 'field.remove'; value: string } | { type: 'field.edit'; value: Field } | { type: 'documentField.createField'; value?: string } | { type: 'documentField.editField'; value: string } - | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus }; + | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus } + | { type: 'documentField.changeEditor'; value: FieldsEditor }; export type Dispatch = (action: Action) => void; export const reducer = (state: State, action: Action): State => { switch (action.type) { - case 'configuration.update': + case 'configuration.update': { + const fieldFormValidity = state.fieldForm === undefined ? true : state.fieldForm.isValid; const isValid = - action.value.isValid === undefined || state.fields.isValid === undefined + action.value.isValid === undefined || fieldFormValidity === undefined ? undefined - : action.value.isValid && state.fields.isValid; + : action.value.isValid && fieldFormValidity; return { ...state, isValid, configuration: action.value, }; + } + case 'fieldForm.update': { + const isValid = + action.value.isValid === undefined || state.configuration.isValid === undefined + ? undefined + : action.value.isValid && state.configuration.isValid; + + return { + ...state, + isValid, + fieldForm: action.value, + }; + } case 'documentField.createField': return { ...state, @@ -78,25 +100,36 @@ export const reducer = (state: State, action: Action): State => { }, }; case 'documentField.changeStatus': - return { ...state, documentFields: { ...state.documentFields, status: action.value } }; + const isValid = action.value === 'idle' ? state.configuration.isValid : state.isValid; + return { + ...state, + isValid, + documentFields: { ...state.documentFields, status: action.value }, + }; + case 'documentField.changeEditor': + return { ...state, documentFields: { ...state.documentFields, editor: action.value } }; case 'field.add': { const id = getUniqueId(); const { fieldToAddFieldTo } = state.documentFields; const addToRootLevel = fieldToAddFieldTo === undefined; + const parentField = addToRootLevel ? undefined : state.fields.byId[fieldToAddFieldTo!]; const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, id] : state.fields.rootLevelFields; + const nestedDepth = parentField ? parentField.nestedDepth + 1 : 0; + const maxNestedDepth = Math.max(state.fields.maxNestedDepth, nestedDepth); + state.fields.byId[id] = { id, parentId: fieldToAddFieldTo, source: action.value, + nestedDepth, ...getFieldMeta(action.value), }; - if (!addToRootLevel) { - const parentField = state.fields.byId[fieldToAddFieldTo!]; + if (parentField) { const childFields = parentField.childFields || []; // Update parent field with new children @@ -109,11 +142,14 @@ export const reducer = (state: State, action: Action): State => { return { ...state, - fields: { ...state.fields, rootLevelFields }, + isValid: state.configuration.isValid, + fieldForm: undefined, + fields: { ...state.fields, rootLevelFields, maxNestedDepth }, }; } case 'field.remove': { - const { id, parentId } = state.fields.byId[action.value]; + const field = state.fields.byId[action.value]; + const { id, parentId, hasChildFields } = field; let { rootLevelFields } = state.fields; if (parentId) { // Deleting a child field @@ -125,12 +161,22 @@ export const reducer = (state: State, action: Action): State => { rootLevelFields = rootLevelFields.filter(childId => childId !== id); } + if (hasChildFields) { + const allChildFields = getAllChildFields(field, state.fields.byId); + allChildFields!.forEach(childField => { + delete state.fields.byId[childField.id]; + }); + } delete state.fields.byId[id]; + + const maxNestedDepth = getMaxNestedDepth(state.fields.byId); + return { ...state, fields: { ...state.fields, rootLevelFields, + maxNestedDepth, }, }; } @@ -163,8 +209,9 @@ export const reducer = (state: State, action: Action): State => { newField.hasChildFields = false; if (previousField.childFields) { - previousField.childFields.forEach(fieldId => { - delete state.fields.byId[fieldId]; + const allChildFields = getAllChildFields(previousField, state.fields.byId); + allChildFields!.forEach(childField => { + delete state.fields.byId[childField.id]; }); } } @@ -172,6 +219,8 @@ export const reducer = (state: State, action: Action): State => { return { ...state, + isValid: state.configuration.isValid, + fieldForm: undefined, documentFields: { ...state.documentFields, status: 'idle', diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts index f2004dd096384..5f63b139f8e92 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/types.ts @@ -112,12 +112,16 @@ export interface NormalizedFields { [id: string]: NormalizedField; }; rootLevelFields: string[]; + maxNestedDepth: number; } export interface NormalizedField extends FieldMeta { id: string; parentId?: string; + nestedDepth: number; source: Omit; } export type ChildFieldName = 'properties' | 'fields'; + +export type FieldsEditor = 'default' | 'json'; From a9aea0afe4941764af7ab9a125a945fa4b72be52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 7 Oct 2019 12:39:24 +0200 Subject: [PATCH 60/64] Reset state when updating documentFields status --- .../mappings_editor/constants/parameters_definition.ts | 10 +++++----- .../static/ui/components/mappings_editor/reducer.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts index 38ee66c0ba088..939ed4364c1fc 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/constants/parameters_definition.ts @@ -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 { FIELD_TYPES, fieldValidators, fieldFormatters } from '../shared_imports'; +import { FIELD_TYPES, fieldValidators, fieldFormatters, FieldConfig } from '../shared_imports'; import { ParameterName, Parameter } from '../types'; const { toInt } = fieldFormatters; @@ -117,7 +117,7 @@ export const PARAMETERS_DEFINITION: { }, }, ], - }, + } as FieldConfig, }, dynamic: { fieldConfig: { @@ -241,7 +241,7 @@ export const PARAMETERS_DEFINITION: { }, }, ], - }, + } as FieldConfig, }, index_prefixes: { fieldConfig: { @@ -317,7 +317,7 @@ export const PARAMETERS_DEFINITION: { }, ], }, - }, + } as { [key: string]: FieldConfig }, }, similarity: { fieldConfig: { @@ -350,6 +350,6 @@ export const PARAMETERS_DEFINITION: { }, }, ], - }, + } as FieldConfig, }, }; diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index c31b2ad9947cf..5713de86c87e1 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -104,7 +104,12 @@ export const reducer = (state: State, action: Action): State => { return { ...state, isValid, - documentFields: { ...state.documentFields, status: action.value }, + documentFields: { + ...state.documentFields, + status: action.value, + fieldToAddFieldTo: undefined, + fieldToEdit: undefined, + }, }; case 'documentField.changeEditor': return { ...state, documentFields: { ...state.documentFields, editor: action.value } }; @@ -144,6 +149,7 @@ export const reducer = (state: State, action: Action): State => { ...state, isValid: state.configuration.isValid, fieldForm: undefined, + documentFields: { ...state.documentFields, fieldToAddFieldTo: undefined }, fields: { ...state.fields, rootLevelFields, maxNestedDepth }, }; } @@ -223,6 +229,7 @@ export const reducer = (state: State, action: Action): State => { fieldForm: undefined, documentFields: { ...state.documentFields, + fieldToEdit: undefined, status: 'idle', }, fields: { From 21d6339fded3e8396e4515dbbcfe35ba68d776a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 7 Oct 2019 12:39:59 +0200 Subject: [PATCH 61/64] Fix form lib hook use_field reset() --- .../static/forms/hook_form_lib/hooks/use_field.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 20651df52e6f2..a24a2640789dd 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -360,12 +360,12 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) }; const reset = () => { - setValue(initialValue); - setErrors([]); setPristine(true); setValidating(false); setIsChangingValue(false); setIsValidated(false); + setErrors([]); + setValue(initialValue); }; const serializeOutput: FieldHook['__serializeOutput'] = (rawValue = value) => From 13dd203419be02b714d20ee8168f6335f8f6e714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 7 Oct 2019 16:40:21 +0200 Subject: [PATCH 62/64] Fix add field reducer --- .../static/ui/components/mappings_editor/reducer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts index 5713de86c87e1..8105f66a8cdf9 100644 --- a/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/static/ui/components/mappings_editor/reducer.ts @@ -104,6 +104,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, isValid, + fieldForm: undefined, documentFields: { ...state.documentFields, status: action.value, @@ -148,8 +149,6 @@ export const reducer = (state: State, action: Action): State => { return { ...state, isValid: state.configuration.isValid, - fieldForm: undefined, - documentFields: { ...state.documentFields, fieldToAddFieldTo: undefined }, fields: { ...state.fields, rootLevelFields, maxNestedDepth }, }; } From 8e631ac4e1454511282687eea439209f2ec9bbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 7 Oct 2019 16:51:49 +0200 Subject: [PATCH 63/64] Fix Index template step logistics --- .../public/components/template_form/steps/step_logistics.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx index 1b20da3a0ee4c..60bc5f139dea9 100644 --- a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_logistics.tsx @@ -83,7 +83,7 @@ export const StepLogistics: React.FunctionComponent = ({ }); useEffect(() => { - onStepValidityChange(form.isValid); + onStepValidityChange(form.isValid === undefined ? true : form.isValid); }, [form.isValid]); useEffect(() => { From 3dcb6095351c360467724328d0467a1618d35fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 7 Oct 2019 18:32:42 +0200 Subject: [PATCH 64/64] Fix 18n keys --- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b7a6b24242924..d69ccb2f693ae 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4741,8 +4741,6 @@ "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "テンプレートを読み込み中…", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "テンプレートの読み込み中にエラーが発生", "xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel": "システムテンプレートを含める", - "xpack.idxMgmt.mappingsEditor.formatError": "JSON フォーマットエラー", - "xpack.idxMgmt.mappingsEditor.mappingsEditorAriaLabel": "インデックスマッピングエディター", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "クローンを作成するテンプレートを読み込み中…", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "クローンを作成するテンプレートを読み込み中にエラーが発生", "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "エイリアスが定義されていません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 79cba3ee696be..8b22dfb2938a9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4744,8 +4744,6 @@ "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "正在加载模板……", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "加载模板时出错", "xpack.idxMgmt.indexTemplatesTable.systemIndexTemplatesSwitchLabel": "包括系统模板", - "xpack.idxMgmt.mappingsEditor.formatError": "JSON 格式错误", - "xpack.idxMgmt.mappingsEditor.mappingsEditorAriaLabel": "索引映射编辑器", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "正在加载要克隆的模板……", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "加载要克隆的模板时出错", "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "未定义任何别名。",