From aa758ea5fed468e49cf981bcb072c6e636d4e9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 9 Oct 2019 10:17:45 +0200 Subject: [PATCH 01/42] Add field settings json editor --- .../public/components/json_editor/index.ts | 2 + .../document_fields/fields/edit_field.tsx | 139 ------------------ .../fields/edit_field/edit_field.tsx | 128 ++++++++++++++++ .../edit_field/edit_field_header_form.tsx | 57 +++++++ .../edit_field/field_settings_json_editor.tsx | 18 +++ .../fields/edit_field/index.ts | 7 + .../update_field_provider.tsx | 6 +- .../document_fields/fields/index.ts | 2 - 8 files changed, 215 insertions(+), 144 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/index.ts rename x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/{ => edit_field}/update_field_provider.tsx (97%) diff --git a/x-pack/legacy/plugins/index_management/public/components/json_editor/index.ts b/x-pack/legacy/plugins/index_management/public/components/json_editor/index.ts index f7f4dc0b30bd6b..d953fb546b1f28 100644 --- a/x-pack/legacy/plugins/index_management/public/components/json_editor/index.ts +++ b/x-pack/legacy/plugins/index_management/public/components/json_editor/index.ts @@ -5,3 +5,5 @@ */ export * from './json_editor'; + +export { OnUpdateHandler } from './use_json'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field.tsx deleted file mode 100644 index a7bdbefa45ffee..00000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field.tsx +++ /dev/null @@ -1,139 +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 { - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiTitle, - EuiButton, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; - -import { - useForm, - Form, - TextField, - SelectField, - UseField, - FieldConfig, - ValidationFunc, -} from '../../../shared_imports'; -import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; -import { useDispatch } from '../../../mappings_state'; -import { Field, NormalizedField, ParameterName } from '../../../types'; -import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; - -const formWrapper = (props: any) =>
; - -const getFieldConfig = (param: ParameterName): FieldConfig => - PARAMETERS_DEFINITION[param].fieldConfig || {}; - -interface Props { - field: NormalizedField; - uniqueNameValidator: ValidationFunc; -} - -export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { - const { form } = useForm({ defaultValue: field.source }); - 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' }); - }; - - const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { - if (e) { - e.preventDefault(); - } - const { isValid, data } = await form.submit(); - if (isValid) { - updateField({ ...field, source: data }); - } - }; - - const cancel = () => { - exitEdit(); - }; - - const { validations, ...rest } = getFieldConfig('name'); - const nameConfig: FieldConfig = { - ...rest, - validations: [ - ...validations!, - { - validator: uniqueNameValidator, - }, - ], - }; - - const renderTempForm = () => ( - - {updateField => ( - - - - - - - - - - - - - Update - - - - Cancel - - - - )} - - ); - - return ( - - - -

Edit field

-
-
- {renderTempForm()} -
- ); -}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx new file mode 100644 index 00000000000000..0ee9d4a334a874 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -0,0 +1,128 @@ +/* + * 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, { useRef, useEffect } from 'react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { useForm, Form, ValidationFunc } from '../../../../shared_imports'; +import { OnUpdateHandler } from '../../../../../json_editor'; +import { useDispatch } from '../../../../mappings_state'; +import { Field, NormalizedField } from '../../../../types'; +import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; +import { EditFieldHeaderForm } from './edit_field_header_form'; +import { FieldSettingsJsonEditor } from './field_settings_json_editor'; + +const formWrapper = (props: any) =>
; + +interface Props { + field: NormalizedField; + uniqueNameValidator: ValidationFunc; +} + +export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { + const { form } = useForm({ defaultValue: field.source }); + const dispatch = useDispatch(); + + const fieldsSettings = useRef[0] | undefined>(undefined); + + useEffect(() => { + const subscription = form.subscribe(updatedFieldForm => { + dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); + }); + + return subscription.unsubscribe; + }, [form]); + + const exitEdit = () => { + dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + }; + + const onFieldsSettingsUpdate: OnUpdateHandler = fieldsSettingsUpdate => { + fieldsSettings.current = fieldsSettingsUpdate; + }; + + const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + const { isValid: isFormValid, data: formData } = await form.submit(); + + const { + isValid: isFieldsSettingsValid, + getData: getFieldsSettingsData, + } = fieldsSettings.current!; + + if (isFormValid && isFieldsSettingsValid) { + const fieldsSettingsData = getFieldsSettingsData(); + updateField({ ...field, source: { ...formData, ...fieldsSettingsData } }); + } + }; + + const cancel = () => { + exitEdit(); + }; + + const { + source: { name, type, ...fieldsSettingsDefault }, + } = field; + + return ( + + {updateField => ( + + + +

Edit field

+
+
+ + + + + + + + + + + + + Update + + + + Cancel + + + +
+ )} +
+ ); +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx new file mode 100644 index 00000000000000..d6c529a163876c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + TextField, + SelectField, + UseField, + FieldConfig, + ValidationFunc, +} from '../../../../shared_imports'; +import { ParameterName } from '../../../../types'; +import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../../constants'; + +const getFieldConfig = (param: ParameterName): FieldConfig => + PARAMETERS_DEFINITION[param].fieldConfig || {}; + +interface Props { + uniqueNameValidator: ValidationFunc; +} + +export const EditFieldHeaderForm = ({ uniqueNameValidator }: Props) => { + const { validations, ...rest } = getFieldConfig('name'); + const nameConfig: FieldConfig = { + ...rest, + validations: [ + ...validations!, + { + validator: uniqueNameValidator, + }, + ], + }; + + return ( + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx new file mode 100644 index 00000000000000..ebf0f6b654ec42 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx @@ -0,0 +1,18 @@ +/* + * 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 { JsonEditor, OnUpdateHandler } from '../../../../../json_editor'; + +interface Props { + onUpdate: OnUpdateHandler; + defaultValue: { [key: string]: any }; +} + +export const FieldSettingsJsonEditor = ({ onUpdate, defaultValue = {} }: Props) => { + return ; +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/index.ts new file mode 100644 index 00000000000000..b8c67e7185ee3f --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/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 './edit_field'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx similarity index 97% rename from x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx rename to x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx index 1245365a1965fb..fa39a86ea1e8ee 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/update_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx @@ -7,9 +7,9 @@ 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'; +import { useState as useMappingsState, useDispatch } from '../../../../mappings_state'; +import { shouldDeleteChildFieldsAfterTypeChange } from '../../../../lib'; +import { NormalizedField, DataType } from '../../../../types'; export type UpdateFieldFunc = (field: NormalizedField) => void; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/index.ts index 0d994d503c66c2..13fe999cea3be4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/index.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/index.ts @@ -13,5 +13,3 @@ export * from './create_field'; export * from './edit_field'; export * from './delete_field_provider'; - -export * from './update_field_provider'; From 1f9f91a262fff27eaed1beae074f41ea47ba76c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 9 Oct 2019 12:14:43 +0200 Subject: [PATCH 02/42] Add isUnmounted ref to form hook --- .../static/forms/hook_form_lib/hooks/use_form.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 87c62611bc0bcc..9364aa48a47971 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 @@ -52,6 +52,7 @@ export function useForm( const [isValid, setIsValid] = useState(undefined); const fieldsRefs = useRef({}); const formUpdateSubscribers = useRef([]); + const isUnmounted = useRef(false); // 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 @@ -64,6 +65,7 @@ export function useForm( return () => { formUpdateSubscribers.current.forEach(subscription => subscription.unsubscribe()); formUpdateSubscribers.current = []; + isUnmounted.current = true; }; }, []); @@ -225,7 +227,9 @@ export function useForm( const validate = async () => await validateAllFields(); const subscription = formData$.current.subscribe(raw => { - handler({ isValid, data: { raw, format }, validate }); + if (!isUnmounted.current) { + handler({ isValid, data: { raw, format }, validate }); + } }); formUpdateSubscribers.current.push(subscription); From 2c7774c6bb4eb9db7f3560dbcee7287429226b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 9 Oct 2019 12:18:16 +0200 Subject: [PATCH 03/42] Update contract for json editor update --- .../public/components/json_editor/use_json.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/json_editor/use_json.ts b/x-pack/legacy/plugins/index_management/public/components/json_editor/use_json.ts index 4e0bc8f5a2c66c..4d8b455d4c26ac 100644 --- a/x-pack/legacy/plugins/index_management/public/components/json_editor/use_json.ts +++ b/x-pack/legacy/plugins/index_management/public/components/json_editor/use_json.ts @@ -10,7 +10,10 @@ import { i18n } from '@kbn/i18n'; import { isJSON } from '../../../../../../../src/plugins/es_ui_shared/static/validators/string'; export type OnUpdateHandler = (arg: { - getData(): T; + data: { + raw: string; + format(): T; + }; validate(): boolean; isValid: boolean; }) => void; @@ -45,7 +48,7 @@ export const useJson = ({ return isValid; }; - const getData = () => { + const formatContent = () => { const isValid = validate(); const data = isValid && content.trim() !== '' ? JSON.parse(content) : {}; return data as T; @@ -54,7 +57,10 @@ export const useJson = ({ useEffect(() => { const isValid = validate(); onUpdate({ - getData, + data: { + raw: content, + format: formatContent, + }, validate, isValid, }); From 25b9195dfb6d18f26240e45d60901f334164d4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 9 Oct 2019 12:29:47 +0200 Subject: [PATCH 04/42] Update useMemo with correct defaultValue --- .../fields/edit_field/edit_field.tsx | 75 ++++++++++++++----- .../edit_field/field_settings_json_editor.tsx | 2 +- .../mappings_editor/mappings_editor.tsx | 30 +++++--- .../template_form/steps/step_mappings.tsx | 19 +++-- 4 files changed, 86 insertions(+), 40 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 0ee9d4a334a874..1aadccd3c43f89 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -15,7 +15,7 @@ import { EuiFlexItem, } from '@elastic/eui'; -import { useForm, Form, ValidationFunc } from '../../../../shared_imports'; +import { useForm, Form, ValidationFunc, OnFormUpdateArg } from '../../../../shared_imports'; import { OnUpdateHandler } from '../../../../../json_editor'; import { useDispatch } from '../../../../mappings_state'; import { Field, NormalizedField } from '../../../../types'; @@ -35,22 +35,7 @@ export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { const dispatch = useDispatch(); const fieldsSettings = useRef[0] | undefined>(undefined); - - useEffect(() => { - const subscription = form.subscribe(updatedFieldForm => { - dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); - }); - - return subscription.unsubscribe; - }, [form]); - - const exitEdit = () => { - dispatch({ type: 'documentField.changeStatus', value: 'idle' }); - }; - - const onFieldsSettingsUpdate: OnUpdateHandler = fieldsSettingsUpdate => { - fieldsSettings.current = fieldsSettingsUpdate; - }; + const fieldFormUpdate = useRef | undefined>(undefined); const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { if (e) { @@ -61,7 +46,7 @@ export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { const { isValid: isFieldsSettingsValid, - getData: getFieldsSettingsData, + data: { format: getFieldsSettingsData }, } = fieldsSettings.current!; if (isFormValid && isFieldsSettingsValid) { @@ -70,6 +55,58 @@ export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { } }; + const getUpdatedField = (): OnFormUpdateArg | void => { + if (fieldFormUpdate.current === undefined || fieldsSettings.current === undefined) { + return; + } + + const isFormValid = fieldFormUpdate.current.isValid; + const isFieldsSettingsValid = fieldsSettings.current.isValid; + + return { + isValid: isFormValid === undefined ? undefined : isFormValid && isFieldsSettingsValid, + data: { + raw: { ...fieldFormUpdate.current.data.raw, settings: fieldsSettings.current.data.raw }, + format() { + return { + ...fieldFormUpdate.current!.data.format(), + ...fieldsSettings.current!.data.format(), + }; + }, + }, + validate: async () => { + const isFieldFormValid = await fieldFormUpdate.current!.validate(); + return isFieldFormValid && fieldsSettings.current!.isValid; + }, + }; + }; + + useEffect(() => { + const subscription = form.subscribe(updatedFieldForm => { + fieldFormUpdate.current = updatedFieldForm; + + const updatedField = getUpdatedField(); + if (updatedField) { + dispatch({ type: 'fieldForm.update', value: updatedField }); + } + }); + + return subscription.unsubscribe; + }, [form]); + + const onFieldsSettingsUpdate: OnUpdateHandler = fieldsSettingsUpdate => { + fieldsSettings.current = fieldsSettingsUpdate; + + const updatedField = getUpdatedField(); + if (updatedField) { + dispatch({ type: 'fieldForm.update', value: updatedField }); + } + }; + + const exitEdit = () => { + dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + }; + const cancel = () => { exitEdit(); }; @@ -90,7 +127,7 @@ export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { > -

Edit field

+

Edit field '{field.source.name}'

diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx index ebf0f6b654ec42..b5cd289f1c9b59 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx @@ -14,5 +14,5 @@ interface Props { } export const FieldSettingsJsonEditor = ({ onUpdate, defaultValue = {} }: Props) => { - return ; + return ; }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx index a292703c8a0cc9..e5af0e7fd8b1ed 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/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 from 'react'; +import React, { useMemo } from 'react'; import { JsonEditor } from '../json_editor'; import { @@ -20,17 +20,23 @@ interface Props { defaultValue?: { [key: string]: any }; } -export const MappingsEditor = React.memo(({ onUpdate, defaultValue = {} }: Props) => { - const configurationDefaultValue = Object.entries(defaultValue) - .filter(([key]) => CONFIGURATION_FIELDS.includes(key)) - .reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: value, - }), - {} as Types['MappingsConfiguration'] - ); - const fieldsDefaultValue = defaultValue.properties || {}; +export const MappingsEditor = React.memo(({ onUpdate, defaultValue }: Props) => { + const configurationDefaultValue = useMemo( + () => + defaultValue === undefined + ? ({} as Types['MappingsConfiguration']) + : Object.entries(defaultValue) + .filter(([key]) => CONFIGURATION_FIELDS.includes(key)) + .reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value, + }), + {} as Types['MappingsConfiguration'] + ), + [defaultValue] + ); + const fieldsDefaultValue = defaultValue === undefined ? {} : defaultValue.properties; // Temporary logic const onJsonEditorUpdate = (args: any) => { diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx index ab3423d4f5d98f..88481ac52a93ef 100644 --- a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx @@ -24,14 +24,17 @@ export const StepMappings: React.FunctionComponent = ({ setDataGetter, onStepValidityChange, }) => { - const onMappingsEditorUpdate = useCallback(({ isValid, getData }) => { - const isMappingsValid = isValid === undefined ? true : isValid; - onStepValidityChange(isMappingsValid); - setDataGetter(() => { - const mappings = getData(); - return Promise.resolve({ isValid: isMappingsValid, data: { mappings } }); - }); - }, []); + const onMappingsEditorUpdate = useCallback( + ({ isValid, getData, validate }) => { + onStepValidityChange(isValid); + setDataGetter(async () => { + const isMappingsValid = isValid === undefined ? await validate() : isValid; + const mappings = getData(); + return Promise.resolve({ isValid: isMappingsValid, data: { mappings } }); + }); + }, + [setDataGetter, onStepValidityChange] + ); return (
From 5218f21f680856f7cce3592422dbf0a041fb9441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 10 Oct 2019 17:04:30 +0200 Subject: [PATCH 05/42] Fix uniqueName validator and wrap NameParameter in its own component --- .../document_fields/document_fields.tsx | 18 ++-------- .../document_fields/field_parameters/index.ts | 7 ++++ .../field_parameters/name_parameter.tsx | 36 +++++++++++++++++++ .../document_fields/fields/create_field.tsx | 30 +++------------- .../fields/edit_field/edit_field.tsx | 7 ++-- .../edit_field/edit_field_header_form.tsx | 29 +++------------ .../fields/fields_list_item.tsx | 11 ++---- .../mappings_editor/mappings_editor.tsx | 16 +++------ 8 files changed, 66 insertions(+), 88 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index 0489a044baa3cd..b4c9be7d4f4ba6 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -4,11 +4,10 @@ * 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'; -import { validateUniqueName } from '../../lib'; import { FieldsList, CreateField, EditField } from './fields'; export const DocumentFields = () => { @@ -21,17 +20,6 @@ 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' }); }; @@ -41,7 +29,7 @@ export const DocumentFields = () => { if (status !== 'creatingField' || fieldToAddFieldTo !== undefined) { return null; } - return ; + return ; }; const renderAddFieldButton = () => { @@ -61,7 +49,7 @@ export const DocumentFields = () => { return null; } const field = byId[fieldToEdit!]; - return ; + return ; }; return ( diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index.ts new file mode 100644 index 00000000000000..fbf37d8303ed9c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/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 './name_parameter'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx new file mode 100644 index 00000000000000..51a20f3e94bb92 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx @@ -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. + */ + +import React from 'react'; + +import { TextField, UseField, FieldConfig } from '../../../shared_imports'; +import { validateUniqueName } from '../../../lib'; +import { PARAMETERS_DEFINITION } from '../../../constants'; +import { useState } from '../../../mappings_state'; + +export const NameParameter = () => { + const { + fields: { rootLevelFields, byId }, + documentFields: { fieldToAddFieldTo, fieldToEdit }, + } = useState(); + const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig; + + const initialName = fieldToEdit ? byId[fieldToEdit].source.name : undefined; + const parentId = fieldToEdit ? byId[fieldToEdit].parentId : fieldToAddFieldTo; + const uniqueNameValidator = validateUniqueName({ rootLevelFields, byId }, initialName, parentId); + + const nameConfig: FieldConfig = { + ...rest, + validations: [ + ...validations!, + { + validator: uniqueNameValidator, + }, + ], + }; + + return ; +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 241f3522a18241..58b556c9807706 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -6,29 +6,18 @@ import React, { useEffect } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - useForm, - Form, - TextField, - SelectField, - UseField, - FieldConfig, - ValidationFunc, -} from '../../../shared_imports'; +import { useForm, Form, SelectField, UseField, FieldConfig } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; import { Field, ParameterName } from '../../../types'; +import { NameParameter } from '../field_parameters'; const formWrapper = (props: any) =>
; const getFieldConfig = (param: ParameterName): FieldConfig => PARAMETERS_DEFINITION[param].fieldConfig || {}; -interface Props { - uniqueNameValidator: ValidationFunc; -} - -export const CreateField = React.memo(({ uniqueNameValidator }: Props) => { +export const CreateField = React.memo(() => { const { form } = useForm(); const dispatch = useDispatch(); @@ -57,22 +46,11 @@ export const CreateField = React.memo(({ uniqueNameValidator }: Props) => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; - const { validations, ...rest } = getFieldConfig('name'); - const nameConfig: FieldConfig = { - ...rest, - validations: [ - ...validations!, - { - validator: uniqueNameValidator, - }, - ], - }; - return ( - + ; interface Props { field: NormalizedField; - uniqueNameValidator: ValidationFunc; } -export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { +export const EditField = React.memo(({ field }: Props) => { const { form } = useForm({ defaultValue: field.source }); const dispatch = useDispatch(); @@ -138,7 +137,7 @@ export const EditField = React.memo(({ field, uniqueNameValidator }: Props) => { FormWrapper={formWrapper} onSubmit={getSubmitForm(updateField)} > - + PARAMETERS_DEFINITION[param].fieldConfig || {}; -interface Props { - uniqueNameValidator: ValidationFunc; -} - -export const EditFieldHeaderForm = ({ uniqueNameValidator }: Props) => { - const { validations, ...rest } = getFieldConfig('name'); - const nameConfig: FieldConfig = { - ...rest, - validations: [ - ...validations!, - { - validator: uniqueNameValidator, - }, - ], - }; - +export const EditFieldHeaderForm = () => { return ( - + { const dispatch = useDispatch(); const { documentFields: { status, fieldToAddFieldTo }, - fields: { byId, rootLevelFields }, + fields: { byId }, } = 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({ type: 'documentField.createField', @@ -65,7 +60,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return (
- +
); }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx index e5af0e7fd8b1ed..9a0a7c2349591f 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx @@ -5,6 +5,7 @@ */ import React, { useMemo } from 'react'; +import { pick } from 'lodash'; import { JsonEditor } from '../json_editor'; import { @@ -23,19 +24,12 @@ interface Props { export const MappingsEditor = React.memo(({ onUpdate, defaultValue }: Props) => { const configurationDefaultValue = useMemo( () => - defaultValue === undefined - ? ({} as Types['MappingsConfiguration']) - : Object.entries(defaultValue) - .filter(([key]) => CONFIGURATION_FIELDS.includes(key)) - .reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: value, - }), - {} as Types['MappingsConfiguration'] - ), + (defaultValue === undefined + ? {} + : pick(defaultValue, CONFIGURATION_FIELDS)) as Types['MappingsConfiguration'], [defaultValue] ); + const fieldsDefaultValue = defaultValue === undefined ? {} : defaultValue.properties; // Temporary logic From b33915c5132dd788046f4fc1cb7891435a4026a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 10 Oct 2019 18:55:03 +0200 Subject: [PATCH 06/42] Move child field create at the bottom of the list --- .../components/document_fields/fields/fields_list_item.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index b1aff5fc6778bc..0a377b209ee41d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -106,13 +106,13 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { )}
- {renderCreateField()} - {hasChildFields && (
)} + + {renderCreateField()} ); }; From c0c706e9e5f91d50de2782ec0b91490b5a754710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 11 Oct 2019 12:19:15 +0200 Subject: [PATCH 07/42] Add initial UI style --- .../components/mappings_editor/_index.scss | 45 ++++++++++++ .../configuration_form/configuration_form.tsx | 2 +- .../document_fields/document_fields.tsx | 8 +- .../document_fields/fields/fields_list.tsx | 4 +- .../fields/fields_list_item.tsx | 73 ++++++++++--------- .../mappings_editor/mappings_editor.tsx | 9 ++- .../index_management/public/index.scss | 1 + 7 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss new file mode 100644 index 00000000000000..9c03a18f7c9937 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -0,0 +1,45 @@ + +// [1] We add a negative margin of 1px to make the border of the
  • overlap the border of the
      + +.mappings-editor { + &__fields-list { + // border-bottom: 1px solid #ddd; + margin-bottom: -1px; // [1] + } + &__fields-list-item { + // padding-bottom: 20px; + // border-bottom: 1px solid #ddd; + + // &:last-child { + // border-bottom: 0; + // } + + &__field { + height: 56px; + + border-bottom: 1px solid #ddd; + margin: 12px 0; + + // border-bottom: '1px solid #ddd', + // display: 'flex', + // flexDirection: 'column' as 'column', + + // > div { + // display: flex; + // align-items: center; + // height: 82px; + // } + } + + &:last-child { + padding-bottom: 0; + } + + &__name { + width: 120px; + } + // &__type { + // width: 80px; + // } + } +} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/configuration_form/configuration_form.tsx index dc9ae6c1582426..2fc9fbb0d5e7c6 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -33,7 +33,7 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { }, [form]); return ( -
      + { }; const renderAddFieldButton = () => { - if (status !== 'idle') { - return null; - } + const isDisabled = status !== 'idle'; return ( <> - Add field + + Add field + ); }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx index fa505a47b0a8dc..a25cd7edacd1b4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -15,9 +15,9 @@ interface Props { export const FieldsList = React.memo(({ fields = [], treeDepth = 0 }: Props) => { return ( -
        +
          {fields.map(field => ( -
        • +
        • ))} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 0a377b209ee41d..c6ee737c5be9a6 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiBadge } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { FieldsList } from './fields_list'; @@ -18,12 +18,6 @@ interface Props { treeDepth?: number; } -const inlineStyle = { - borderBottom: '1px solid #ddd', - display: 'flex', - flexDirection: 'column' as 'column', -}; - export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { @@ -49,12 +43,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { }; const renderCreateField = () => { - if (status !== 'creatingField') { - return null; - } - - // Root level (0) has does not have the "fieldToAddFieldTo" set - if (fieldToAddFieldTo !== id) { + if (status !== 'creatingField' || fieldToAddFieldTo !== id) { return null; } @@ -71,29 +60,45 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { } return ( - <> - Edit - {canHaveChildFields && ( - <> - - Add field - - - )} - - {deleteField => deleteField(field)}>Remove} - - + + + {canHaveChildFields && ( + <> + + Add child + + + )} + + + Edit + + + + {deleteField => ( + deleteField(field)}>Remove + )} + + + ); }; return ( <> -
          -
          - {source.name} | {source.type} {renderActionButtons()} -
          - {status === 'idle' && canHaveChildFields && isAddFieldBtnDisabled && ( +
          + + + {source.name} + + + {source.type} + + + {renderActionButtons()} + + + {/* {status === 'idle' && canHaveChildFields && isAddFieldBtnDisabled && (

          You have reached the maximum depth for the mappings editor. Switch to the{' '} { to add more fields.

          - )} + )} */}
          {hasChildFields && ( -
          - -
          + )} {renderCreateField()} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx index 52aabebcea3d94..b6f54f31d752de 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx @@ -45,13 +45,14 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue }: Props) => }; return ( - <> +
          + {renderEditor()} - - - + {/* + */} +
          ); }} diff --git a/x-pack/legacy/plugins/index_management/public/index.scss b/x-pack/legacy/plugins/index_management/public/index.scss index 27b1ba44597bca..942cd2518f76cb 100644 --- a/x-pack/legacy/plugins/index_management/public/index.scss +++ b/x-pack/legacy/plugins/index_management/public/index.scss @@ -11,3 +11,4 @@ // indChart__legend-isLoading @import 'index_management'; +@import './components/mappings_editor/index'; From 6c9b50455d586603017058ac766b8e18b1ea591a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 11 Oct 2019 13:35:39 +0200 Subject: [PATCH 08/42] Add indent tree + toggle of child fields --- .../components/mappings_editor/_index.scss | 45 +++++++++++++------ .../fields/fields_list_item.tsx | 44 +++++++++++++++--- .../components/mappings_editor/lib/utils.ts | 1 + .../components/mappings_editor/reducer.ts | 22 +++++++++ .../components/mappings_editor/types.ts | 1 + 5 files changed, 94 insertions(+), 19 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 9c03a18f7c9937..4255f3ac040c3a 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -1,24 +1,41 @@ - -// [1] We add a negative margin of 1px to make the border of the
        • overlap the border of the
            - .mappings-editor { &__fields-list { // border-bottom: 1px solid #ddd; - margin-bottom: -1px; // [1] + + + .mappings-editor__fields-list { + padding-left: 20px; + } + + .mappings-editor__fields-list { + .mappings-editor__fields-list-item__toggle { + &::before { + border-bottom: 1px solid #333; + content: ''; + left: -8px; + position: absolute; + top: 26px; + width: 8px; + } + &::after { + border-left: 1px solid #333; + content: ''; + left: -8px; + position: absolute; + top: 18px; + height: 8px; + } + } + } } &__fields-list-item { // padding-bottom: 20px; // border-bottom: 1px solid #ddd; - // &:last-child { - // border-bottom: 0; - // } - &__field { - height: 56px; - - border-bottom: 1px solid #ddd; + border-bottom: 1px dotted #ddd; + // height: 56px; margin: 12px 0; + // position: relative; // border-bottom: '1px solid #ddd', // display: 'flex', @@ -38,8 +55,8 @@ &__name { width: 120px; } - // &__type { - // width: 80px; - // } + &__toggle { + width: 24px; + } } } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index c6ee737c5be9a6..2f81fc03836fee 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiBadge } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiIcon, + EuiBadge, + EuiButtonIcon, +} from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { FieldsList } from './fields_list'; @@ -25,8 +32,17 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { fields: { byId }, } = useState(); const getField = (propId: string) => byId[propId]; - const { id, source, childFields, hasChildFields, canHaveChildFields } = field; + const { + id, + source, + childFields, + hasChildFields, + canHaveChildFields, + nestedDepth, + isExpanded, + } = field; const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; + const indent = `${nestedDepth * 24}px`; const addField = () => { dispatch({ @@ -42,6 +58,10 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { }); }; + const toggleExpand = () => { + dispatch({ type: 'field.toggleExpand', value: { fieldId: id } }); + }; + const renderCreateField = () => { if (status !== 'creatingField' || fieldToAddFieldTo !== id) { return null; @@ -86,8 +106,22 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( <> -
            - +
            + + + {hasChildFields && ( + + )} + {source.name} @@ -111,7 +145,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { )} */}
            - {hasChildFields && ( + {hasChildFields && isExpanded && ( )} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 6e426b686bff04..b031f5a0268023 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -48,6 +48,7 @@ export const getFieldMeta = (field: Field): FieldMeta => { hasChildFields, childFieldsName, canHaveChildFields, + isExpanded: false, }; }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index e7623394390a5b..e307ab0f261ada 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -53,6 +53,7 @@ export type Action = | { type: 'field.add'; value: Field } | { type: 'field.remove'; value: string } | { type: 'field.edit'; value: Field } + | { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } } | { type: 'documentField.createField'; value?: string } | { type: 'documentField.editField'; value: string } | { type: 'documentField.changeStatus'; value: DocumentFieldsStatus } @@ -159,6 +160,7 @@ export const reducer = (state: State, action: Action): State => { ...parentField, childFields: [id, ...childFields], hasChildFields: true, + isExpanded: true, }; } @@ -256,6 +258,26 @@ export const reducer = (state: State, action: Action): State => { }, }; } + case 'field.toggleExpand': { + const updatedField: NormalizedField = { + ...state.fields.byId[action.value.fieldId], + isExpanded: + action.value.isExpanded === undefined + ? !state.fields.byId[action.value.fieldId].isExpanded + : action.value.isExpanded, + }; + + return { + ...state, + fields: { + ...state.fields, + byId: { + ...state.fields.byId, + [action.value.fieldId]: updatedField, + }, + }, + }; + } case 'fieldsJsonEditor.update': { const nextState = { ...state, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 5f63b139f8e92d..d5ba60065a65c5 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -105,6 +105,7 @@ export interface FieldMeta { canHaveChildFields: boolean; hasChildFields: boolean; childFields?: string[]; + isExpanded: boolean; } export interface NormalizedFields { From db04fab988006ab6d26bc6b3bc77e9c59088fe3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 11 Oct 2019 15:51:58 +0200 Subject: [PATCH 09/42] Fix nested css indicator position --- .../public/components/mappings_editor/_index.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 4255f3ac040c3a..3048bdf29ab1e4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -13,7 +13,7 @@ content: ''; left: -8px; position: absolute; - top: 26px; + top: 27px; width: 8px; } &::after { @@ -21,7 +21,7 @@ content: ''; left: -8px; position: absolute; - top: 18px; + top: 19px; height: 8px; } } From d49c3f44b4931a038ffa6632a1c60bf1962423a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 11 Oct 2019 16:46:21 +0200 Subject: [PATCH 10/42] First pass at styling CreateField component --- .../document_fields/fields/create_field.tsx | 56 +++++++++++-------- .../fields/fields_list_item.tsx | 20 +++---- .../components/mappings_editor/reducer.ts | 2 +- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 58b556c9807706..a92325128c702e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useEffect } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useForm, Form, SelectField, UseField, FieldConfig } from '../../../shared_imports'; import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; @@ -47,30 +47,38 @@ export const CreateField = React.memo(() => { }; return ( - - - - + + + + + + + + + + + - - - - - - Add - - - - Cancel + + + + Cancel + + + + Add + + + diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 2f81fc03836fee..3e6a8d0d16ef76 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiIcon, - EuiBadge, - EuiButtonIcon, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiBadge, EuiButtonIcon } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { FieldsList } from './fields_list'; @@ -43,6 +36,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { } = field; const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; const indent = `${nestedDepth * 24}px`; + const indentChild = `${(nestedDepth + 1) * 24}px`; const addField = () => { dispatch({ @@ -68,7 +62,14 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { } return ( -
            +
            ); @@ -148,7 +149,6 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { {hasChildFields && isExpanded && ( )} - {renderCreateField()} ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index e307ab0f261ada..c8400e253fe848 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -158,7 +158,7 @@ export const reducer = (state: State, action: Action): State => { // Update parent field with new children state.fields.byId[fieldToAddFieldTo!] = { ...parentField, - childFields: [id, ...childFields], + childFields: [...childFields, id], hasChildFields: true, isExpanded: true, }; From 0b466963f3be72b7f75c732474f27c4e14e0e55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 11 Oct 2019 16:57:58 +0200 Subject: [PATCH 11/42] Add css to hide actions until mouse over --- .../components/mappings_editor/_index.scss | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 3048bdf29ab1e4..8132853fe34c61 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -28,28 +28,15 @@ } } &__fields-list-item { - // padding-bottom: 20px; - // border-bottom: 1px solid #ddd; - &__field { border-bottom: 1px dotted #ddd; - // height: 56px; margin: 12px 0; - // position: relative; - - // border-bottom: '1px solid #ddd', - // display: 'flex', - // flexDirection: 'column' as 'column', - - // > div { - // display: flex; - // align-items: center; - // height: 82px; - // } - } - &:last-child { - padding-bottom: 0; + &:hover { + .mappings-editor__fields-list-item__actions { + opacity: 1; + } + } } &__name { @@ -58,5 +45,9 @@ &__toggle { width: 24px; } + &__actions { + opacity: 0; + transition: opacity 250ms cubic-bezier(0.694, 0.0482, 0.335, 1); + } } } From bf787d721e7dae7ab8543a29d36d11c4fa848de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 15 Oct 2019 14:36:48 +0200 Subject: [PATCH 12/42] Improve fields list styling --- .../components/mappings_editor/_index.scss | 87 +++++++++++-------- .../document_fields/document_fields.tsx | 9 +- .../fields/fields_list_item.tsx | 83 +++++++++--------- .../mappings_editor/mappings_editor.tsx | 1 - 4 files changed, 101 insertions(+), 79 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 8132853fe34c61..4121ae3e7746bb 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -1,53 +1,72 @@ .mappings-editor { - &__fields-list { - // border-bottom: 1px solid #ddd; - - + .mappings-editor__fields-list { - padding-left: 20px; - } - - .mappings-editor__fields-list { - .mappings-editor__fields-list-item__toggle { - &::before { - border-bottom: 1px solid #333; - content: ''; - left: -8px; - position: absolute; - top: 27px; - width: 8px; - } - &::after { - border-left: 1px solid #333; - content: ''; - left: -8px; - position: absolute; - top: 19px; - height: 8px; - } - } - } - } &__fields-list-item { &__field { - border-bottom: 1px dotted #ddd; - margin: 12px 0; + border-bottom: $euiBorderThin; &:hover { + background-color: $euiColorLightestShade; .mappings-editor__fields-list-item__actions { opacity: 1; } } } - &__name { - width: 120px; + &__content { + height: $euiSizeXL * 2; + position: relative; } + &__toggle { - width: 24px; + padding-left: $euiSizeXS; + width: $euiSizeL; } + &__actions { opacity: 0; - transition: opacity 250ms cubic-bezier(0.694, 0.0482, 0.335, 1); + transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; + } + } + + &__create-field-wrapper { + background-color: $euiColorLightShade; + padding: $euiSizeM; + } + + &__create-field-content { + position: relative; + + form { + padding-left: $euiSizeXL; } } } + +.mappings-editor__fields-list { + .mappings-editor__fields-list .mappings-editor__fields-list-item__content, + .mappings-editor__create-field-content { + &::before { + border-bottom: 1px solid #333; + content: ''; + left: $euiSize; + position: absolute; + top: 50%; + width: $euiSizeS; + } + &::after { + border-left: 1px solid #333; + content: ''; + left: $euiSize; + position: absolute; + top: calc(50% - #{$euiSizeS}); + height: $euiSizeS; + } + } + .mappings-editor__fields-list .mappings-editor__fields-list-item__content--toggle { + &::before{ + content: none; + } + &::after { + content: none; + } + } +} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index 33b8788d4dcc26..0d85fdfaa3f9a3 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -29,7 +29,14 @@ export const DocumentFields = () => { if (status !== 'creatingField' || fieldToAddFieldTo !== undefined) { return null; } - return ; + + return ( +
            +
            + +
            +
            + ); }; const renderAddFieldButton = () => { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 3e6a8d0d16ef76..4df9b0b53ac8cd 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiBadge, EuiButtonIcon } from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; @@ -18,6 +19,8 @@ interface Props { treeDepth?: number; } +const INDENT_SIZE = 32; + export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { @@ -35,8 +38,8 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { isExpanded, } = field; const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; - const indent = `${nestedDepth * 24}px`; - const indentChild = `${(nestedDepth + 1) * 24}px`; + const indent = `${nestedDepth * INDENT_SIZE}px`; + const indentChild = `${(nestedDepth + 1) * INDENT_SIZE}px`; const addField = () => { dispatch({ @@ -63,14 +66,14 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return (
            - +
            + +
            ); }; @@ -108,47 +111,41 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( <>
            - - - {hasChildFields && ( - - )} - - - {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. -

            - )} */} +
            + + + {hasChildFields && ( + + )} + + + {source.name} + + + {source.type} + + + {renderActionButtons()} + + +
            {hasChildFields && isExpanded && ( )} + {renderCreateField()} ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx index b6f54f31d752de..fd097d70a82f23 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx @@ -14,7 +14,6 @@ import { DocumentFieldsHeaders, DocumentFields, DocumentFieldsJsonEditor, - EditorToggleControls, } from './components'; import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state'; From a546907434e52988ffd71e25431e707aa20fd746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 16 Oct 2019 12:43:52 +0200 Subject: [PATCH 13/42] Highlight field being edited --- .../components/mappings_editor/_index.scss | 32 +++++++++++++--- .../fields/fields_list_item.tsx | 37 ++++++++++++++----- .../components/mappings_editor/lib/utils.ts | 19 +++++++--- .../components/mappings_editor/types.ts | 2 + 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 4121ae3e7746bb..3c53f7f7fb71ae 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -1,3 +1,9 @@ + +/* + [1] When the component is embedded inside the tree, we need + to add some extra indent to make room for the child "bullet" on the left. + */ + .mappings-editor { &__fields-list-item { &__field { @@ -9,6 +15,21 @@ opacity: 1; } } + + &--selected { + background-color: $euiColorLightestShade; + &:hover { + background-color: $euiColorLightestShade; + } + } + + &--dim { + opacity: 0.3; + + &:hover { + background-color: initial; + } + } } &__content { @@ -29,15 +50,11 @@ &__create-field-wrapper { background-color: $euiColorLightShade; - padding: $euiSizeM; + padding: $euiSize; } &__create-field-content { position: relative; - - form { - padding-left: $euiSizeXL; - } } } @@ -61,6 +78,11 @@ height: $euiSizeS; } } + + .mappings-editor__create-field-content { + padding-left: $euiSizeXL; // [1] + } + .mappings-editor__fields-list .mappings-editor__fields-list-item__content--toggle { &::before{ content: none; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 4df9b0b53ac8cd..844f94a4abbf74 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -24,7 +24,7 @@ const INDENT_SIZE = 32; export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { - documentFields: { status, fieldToAddFieldTo }, + documentFields: { status, fieldToAddFieldTo, fieldToEdit }, fields: { byId }, } = useState(); const getField = (propId: string) => byId[propId]; @@ -32,8 +32,10 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { id, source, childFields, - hasChildFields, canHaveChildFields, + hasChildFields, + canHaveMultiFields, + hasMultiFields, nestedDepth, isExpanded, } = field; @@ -85,15 +87,18 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( - - {canHaveChildFields && ( - <> + {(canHaveMultiFields || canHaveChildFields) && ( + + {canHaveChildFields && ( Add child - - )} - + )} + {canHaveMultiFields && ( + Multi-fields + )} + + )} Edit @@ -110,7 +115,14 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( <> -
            +
            { {source.name} - + {source.type} + {hasMultiFields && ( + + {`${childFields!.length} multi-field`} + + )} {renderActionButtons()} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index b031f5a0268023..7f6954aa7a980c 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -38,16 +38,25 @@ const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { export const getFieldMeta = (field: Field): FieldMeta => { const childFieldsName = getChildFieldsName(field.type); - const canHaveChildFields = Boolean(childFieldsName); + + const canHaveChildFields = childFieldsName === 'properties'; const hasChildFields = - childFieldsName !== undefined && - Boolean(field[childFieldsName]) && - Object.keys(field[childFieldsName]!).length > 0; + canHaveChildFields && + Boolean(field[childFieldsName!]) && + Object.keys(field[childFieldsName!]!).length > 0; + + const canHaveMultiFields = childFieldsName === 'fields'; + const hasMultiFields = + canHaveMultiFields && + Boolean(field[childFieldsName!]) && + Object.keys(field[childFieldsName!]!).length > 0; return { - hasChildFields, childFieldsName, canHaveChildFields, + hasChildFields, + canHaveMultiFields, + hasMultiFields, isExpanded: false, }; }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index d5ba60065a65c5..e80c55d794ae9e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -103,7 +103,9 @@ export interface Field { export interface FieldMeta { childFieldsName: ChildFieldName | undefined; canHaveChildFields: boolean; + canHaveMultiFields: boolean; hasChildFields: boolean; + hasMultiFields: boolean; childFields?: string[]; isExpanded: boolean; } From 072f1de6badfde1760d57444312fdc47a9049668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 16 Oct 2019 17:22:35 +0200 Subject: [PATCH 14/42] Add path meta to fields --- .../components/mappings_editor/_index.scss | 2 +- .../document_fields/fields/create_field.tsx | 6 +- .../fields/edit_field/edit_field.tsx | 2 + .../mappings_editor/lib/utils.test.ts | 88 ++++++------- .../components/mappings_editor/lib/utils.ts | 117 ++++++++++++++++-- .../components/mappings_editor/reducer.ts | 36 ++++-- .../components/mappings_editor/types.ts | 5 +- 7 files changed, 188 insertions(+), 68 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 3c53f7f7fb71ae..f965f23d16498a 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -80,7 +80,7 @@ } .mappings-editor__create-field-content { - padding-left: $euiSizeXL; // [1] + padding-left: $euiSizeXXL - $euiSizeXS; // [1] } .mappings-editor__fields-list .mappings-editor__fields-list-item__content--toggle { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index a92325128c702e..2d3b6a22059d0f 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -48,9 +48,9 @@ export const CreateField = React.memo(() => { return (
            - + - + @@ -69,7 +69,7 @@ export const CreateField = React.memo(() => { - + Cancel diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 2711e4919634c9..a5867508b974d4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -13,6 +13,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiCode, } from '@elastic/eui'; import { useForm, Form, OnFormUpdateArg } from '../../../../shared_imports'; @@ -128,6 +129,7 @@ export const EditField = React.memo(({ field }: Props) => {

            Edit field '{field.source.name}'

            + {field.path} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts index 39912a0cf0c86f..4aa8aaf7c71a79 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts @@ -6,58 +6,60 @@ jest.mock('../constants', () => ({ DATA_TYPE_DEFINITION: {} })); -import { determineIfValid } from '.'; - -describe('Mappings Editor form validity', () => { - let components: any; - it('handles base case', () => { - components = { - fieldsJsonEditor: { isValid: undefined }, - configuration: { isValid: undefined }, - fieldForm: undefined, - }; - expect(determineIfValid(components)).toBe(undefined); - }); +import { isStateValid } from './utils'; + +describe('utils', () => { + describe('isStateValid()', () => { + let components: any; + it('handles base case', () => { + components = { + fieldsJsonEditor: { isValid: undefined }, + configuration: { isValid: undefined }, + fieldForm: undefined, + }; + expect(isStateValid(components)).toBe(undefined); + }); - it('handles combinations of true, false and undefined', () => { - components = { - fieldsJsonEditor: { isValid: false }, - configuration: { isValid: true }, - fieldForm: undefined, - }; + it('handles combinations of true, false and undefined', () => { + components = { + fieldsJsonEditor: { isValid: false }, + configuration: { isValid: true }, + fieldForm: undefined, + }; - expect(determineIfValid(components)).toBe(false); + expect(isStateValid(components)).toBe(false); - components = { - fieldsJsonEditor: { isValid: false }, - configuration: { isValid: undefined }, - fieldForm: undefined, - }; + components = { + fieldsJsonEditor: { isValid: false }, + configuration: { isValid: undefined }, + fieldForm: undefined, + }; - expect(determineIfValid(components)).toBe(undefined); + expect(isStateValid(components)).toBe(undefined); - components = { - fieldsJsonEditor: { isValid: true }, - configuration: { isValid: undefined }, - fieldForm: undefined, - }; + components = { + fieldsJsonEditor: { isValid: true }, + configuration: { isValid: undefined }, + fieldForm: undefined, + }; - expect(determineIfValid(components)).toBe(undefined); + expect(isStateValid(components)).toBe(undefined); - components = { - fieldsJsonEditor: { isValid: true }, - configuration: { isValid: false }, - fieldForm: undefined, - }; + components = { + fieldsJsonEditor: { isValid: true }, + configuration: { isValid: false }, + fieldForm: undefined, + }; - expect(determineIfValid(components)).toBe(false); + expect(isStateValid(components)).toBe(false); - components = { - fieldsJsonEditor: { isValid: false }, - configuration: { isValid: true }, - fieldForm: { isValid: true }, - }; + components = { + fieldsJsonEditor: { isValid: false }, + configuration: { isValid: true }, + fieldForm: { isValid: true }, + }; - expect(determineIfValid(components)).toBe(false); + expect(isStateValid(components)).toBe(false); + }); }); }); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 7f6954aa7a980c..515e05800d09dc 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -19,11 +19,18 @@ import { DATA_TYPE_DEFINITION, MAX_DEPTH_DEFAULT_EDITOR } from '../constants'; import { State } from '../reducer'; export const getUniqueId = () => { + const dateNow = Date.now(); + + let performanceNow: number; + try { + performanceNow = performance.now(); // only available in the browser + } catch { + performanceNow = process.hrtime()[0]; // only in Node + } + return ( '_' + - (Number(String(Math.random()).slice(2)) + Date.now() + Math.round(performance.now())).toString( - 36 - ) + (Number(String(Math.random()).slice(2)) + dateNow + Math.round(performanceNow)).toString(36) ); }; @@ -138,7 +145,8 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { const normalizeFields = ( props: Fields, - to: NormalizedFields['byId'] = {}, + to: NormalizedFields['byId'], + paths: string[], idsArray: string[], nestedDepth: number, parentId?: string @@ -153,7 +161,14 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { if (childFieldsName && field[childFieldsName]) { meta.childFields = []; maxNestedDepth = Math.max(maxNestedDepth, nestedDepth + 1); - normalizeFields(field[meta.childFieldsName!]!, to, meta.childFields, nestedDepth + 1, id); + normalizeFields( + field[meta.childFieldsName!]!, + to, + [...paths, propName], + meta.childFields, + nestedDepth + 1, + id + ); } const { properties, fields, ...rest } = field; @@ -162,6 +177,7 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { id, parentId, nestedDepth, + path: paths.length ? `${paths.join('.')}.${propName}` : propName, source: rest, ...meta, }; @@ -172,7 +188,7 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { }, to); const rootLevelFields: string[] = []; - const byId = normalizeFields(fieldsToNormalize, {}, rootLevelFields, 0); + const byId = normalizeFields(fieldsToNormalize, {}, [], rootLevelFields, 0); return { byId, @@ -199,6 +215,43 @@ export const deNormalize = (normalized: NormalizedFields): Fields => { return deNormalizePaths(normalized.rootLevelFields); }; +/** + * If we change the "name" of a field, we need to update its `path` and the + * one of **all** of its child properties or multi-fields. + * + * @param field The field who's name has changed + * @param byId The map of all the document fields + */ +export const updateFieldsPathAfterFieldNameChange = ( + field: NormalizedField, + byId: NormalizedFields['byId'] +): { path: string; byId: NormalizedFields['byId'] } => { + const updatedById = { ...byId }; + const paths = field.parentId ? byId[field.parentId].path.split('.') : []; + + const updateFieldPath = (_field: NormalizedField, _paths: string[]): void => { + const { name } = _field.source; + const path = _paths.length === 0 ? name : `${_paths.join('.')}.${name}`; + + updatedById[_field.id] = { + ..._field, + path, + }; + + if (_field.hasChildFields || _field.hasMultiFields) { + _field + .childFields!.map(fieldId => byId[fieldId]) + .forEach(childField => { + updateFieldPath(childField, [..._paths, name]); + }); + } + }; + + updateFieldPath(field, paths); + + return { path: updatedById[field.id].path, byId: updatedById }; +}; + /** * Retrieve recursively all the children fields of a field * @@ -210,7 +263,7 @@ export const getAllChildFields = ( byId: NormalizedFields['byId'] ): NormalizedField[] => { const getChildFields = (_field: NormalizedField, to: NormalizedField[] = []) => { - if (_field.hasChildFields) { + if (_field.hasChildFields || _field.hasMultiFields) { _field .childFields!.map(fieldId => byId[fieldId]) .forEach(childField => { @@ -260,7 +313,7 @@ export const canUseMappingsEditor = (maxNestedDepth: number) => const stateWithValidity: Array = ['configuration', 'fieldsJsonEditor', 'fieldForm']; -export const determineIfValid = (state: State): boolean | undefined => +export const isStateValid = (state: State): boolean | undefined => Object.entries(state) .filter(([key]) => stateWithValidity.includes(key as keyof State)) .reduce( @@ -278,3 +331,51 @@ export const determineIfValid = (state: State): boolean | undefined => }, true as undefined | boolean ); + +// const data = { +// title: { +// type: 'text', +// }, +// address: { +// type: 'object', +// properties: { +// street: { +// type: 'text', +// fields: { +// raw: { +// type: 'keyword', +// }, +// }, +// }, +// geo: { +// type: 'object', +// properties: { +// long: { +// type: 'text', +// }, +// lat: { +// type: 'text', +// }, +// }, +// }, +// }, +// }, +// }; + +// const normalized = normalize(data); + +// const { byId } = normalized; +// let addressField; + +// Object.entries(byId).forEach(([id, normalizedField]) => { +// if (normalizedField.path === 'address.street') { +// addressField = normalizedField; +// } +// }); + +// addressField.source.name = 'changed'; +// const updatedById = updateFieldsPathAfterFieldNameChange(addressField, byId); + +// // console.log(JSON.stringify(normalized, null, 4)); +// console.log(JSON.stringify(addressField, null, 4)); +// console.log(JSON.stringify(updatedById, null, 4)); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index c8400e253fe848..5f8248f2cbf1a4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -11,8 +11,9 @@ import { shouldDeleteChildFieldsAfterTypeChange, getAllChildFields, getMaxNestedDepth, - determineIfValid, + isStateValid, normalize, + updateFieldsPathAfterFieldNameChange, } from './lib'; export interface MappingsConfiguration { @@ -67,7 +68,7 @@ export const reducer = (state: State, action: Action): State => { case 'configuration.update': { return { ...state, - isValid: determineIfValid({ + isValid: isStateValid({ ...state, configuration: action.value, }), @@ -77,7 +78,7 @@ export const reducer = (state: State, action: Action): State => { case 'fieldForm.update': { return { ...state, - isValid: determineIfValid({ + isValid: isStateValid({ ...state, fieldForm: action.value, }), @@ -140,14 +141,16 @@ export const reducer = (state: State, action: Action): State => { 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); + const { name } = action.value; + const path = parentField ? `${parentField.path}.${name}` : name; state.fields.byId[id] = { id, parentId: fieldToAddFieldTo, source: action.value, + path, nestedDepth, ...getFieldMeta(action.value), }; @@ -166,7 +169,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, - isValid: determineIfValid(state), + isValid: isStateValid(state), fields: { ...state.fields, rootLevelFields, maxNestedDepth }, }; } @@ -240,9 +243,23 @@ export const reducer = (state: State, action: Action): State => { } } + let updatedById: NormalizedFields['byId']; + const nameHasChanged = newField.source.name !== previousField.source.name; + + if (nameHasChanged) { + // If the name has changed, we need to update the `path` of the field and recursively + // the paths of all its "descendant" fields (child or multi-field) + const { path, byId } = updateFieldsPathAfterFieldNameChange(newField, state.fields.byId); + updatedById = byId; + updatedById[fieldToEdit] = { ...newField, path }; + } else { + updatedById = { ...state.fields.byId }; + updatedById[fieldToEdit] = newField; + } + return { ...state, - isValid: determineIfValid(state), + isValid: isStateValid(state), fieldForm: undefined, documentFields: { ...state.documentFields, @@ -251,10 +268,7 @@ export const reducer = (state: State, action: Action): State => { }, fields: { ...state.fields, - byId: { - ...state.fields.byId, - [fieldToEdit]: newField, - }, + byId: updatedById, }, }; } @@ -289,7 +303,7 @@ export const reducer = (state: State, action: Action): State => { }, }; - nextState.isValid = determineIfValid(nextState); + nextState.isValid = isStateValid(nextState); return nextState; } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index e80c55d794ae9e..cc365e2602f3ab 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -96,8 +96,8 @@ export interface Fields { export interface Field { name: string; type: DataType; - properties?: { [key: string]: Field }; - fields?: { [key: string]: Field }; + properties?: { [key: string]: Omit }; + fields?: { [key: string]: Omit }; } export interface FieldMeta { @@ -122,6 +122,7 @@ export interface NormalizedField extends FieldMeta { id: string; parentId?: string; nestedDepth: number; + path: string; source: Omit; } From 991aa7546b4d67b0deee0fcc5f803f73a912db35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 17 Oct 2019 12:10:26 +0200 Subject: [PATCH 15/42] Update button style in flyout footer --- .../fields/edit_field/edit_field.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index a5867508b974d4..b005e7ab02d1c6 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -11,6 +11,7 @@ import { EuiFlyoutFooter, EuiTitle, EuiButton, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiCode, @@ -148,15 +149,15 @@ export const EditField = React.memo(({ field }: Props) => { - - - + + + Cancel + + + Update - - Cancel - From 480521798e3725158890f11bd2a1fdfe73d8a348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 17 Oct 2019 12:31:04 +0200 Subject: [PATCH 16/42] Update document fields header + add field button --- .../document_fields/document_fields.tsx | 6 +++--- .../document_fields_header.tsx | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index 0d85fdfaa3f9a3..02c0f888b2bdb0 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiButton, EuiSpacer } from '@elastic/eui'; +import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { useState, useDispatch } from '../../mappings_state'; import { FieldsList, CreateField, EditField } from './fields'; @@ -44,9 +44,9 @@ export const DocumentFields = () => { return ( <> - + Add field - + ); }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_header.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_header.tsx index e3cade87436106..ebf54e52bb30d2 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_header.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields_header.tsx @@ -4,12 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiTitle } from '@elastic/eui'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; export const DocumentFieldsHeaders = () => { return ( - -

            Document fields

            -
            + <> + +

            + {i18n.translate('xpack.idxMgmt.mappingsEditor.documentFieldsTitle', { + defaultMessage: 'Document fields', + })} +

            +
            + + + {i18n.translate('xpack.idxMgmt.mappingsEditor.documentFieldsDescription', { + defaultMessage: 'Define which fields the documents of your index will contain.', + })} + + ); }; From 8f61a98372d27a60a65244a00f13f85c198b6e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 17 Oct 2019 12:49:42 +0200 Subject: [PATCH 17/42] Show the component on empty list When there aren't any fields defined on the document, we show by default the "CreateField" component to add a field. --- .../components/mappings_editor/_index.scss | 1 + .../document_fields/document_fields.tsx | 14 +++++++++++--- .../document_fields/fields/create_field.tsx | 16 +++++++++++----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index f965f23d16498a..5166892c7f8772 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -44,6 +44,7 @@ &__actions { opacity: 0; + padding-right: $euiSizeS; transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; } } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index 02c0f888b2bdb0..83adfb89ad52b3 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { useState, useDispatch } from '../../mappings_state'; @@ -24,16 +24,24 @@ export const DocumentFields = () => { dispatch({ type: 'documentField.createField' }); }; + useEffect(() => { + if (status === 'idle' && fields.length === 0) { + addField(); + } + }, [fields, status]); + const renderCreateField = () => { // The "fieldToAddFieldTo" is undefined when adding to the top level "properties" object. - if (status !== 'creatingField' || fieldToAddFieldTo !== undefined) { + const showCreateField = status === 'creatingField' && fieldToAddFieldTo === undefined; + + if (!showCreateField) { return null; } return (
            - + 0} />
            ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 2d3b6a22059d0f..2bf3ef89e99c8d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -17,7 +17,11 @@ const formWrapper = (props: any) => ; const getFieldConfig = (param: ParameterName): FieldConfig => PARAMETERS_DEFINITION[param].fieldConfig || {}; -export const CreateField = React.memo(() => { +interface Props { + isCancelable?: boolean; +} + +export const CreateField = React.memo(({ isCancelable = true }: Props) => { const { form } = useForm(); const dispatch = useDispatch(); @@ -48,7 +52,7 @@ export const CreateField = React.memo(() => { return ( - + @@ -70,9 +74,11 @@ export const CreateField = React.memo(() => { - - Cancel - + {isCancelable && ( + + Cancel + + )} Add From 92e1cb8b4199b140e80254300f0529f1b3295e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 17 Oct 2019 14:58:05 +0200 Subject: [PATCH 18/42] Add subType select to "create" and "edit" --- .../document_fields/fields/create_field.tsx | 127 +++++++++++++----- .../fields/edit_field/edit_field.tsx | 12 +- .../edit_field/edit_field_header_form.tsx | 88 +++++++++--- .../fields/fields_list_item.tsx | 4 +- .../constants/default_values.ts | 12 ++ .../mappings_editor/constants/index.ts | 2 + .../constants/parameters_definition.ts | 7 +- .../components/mappings_editor/lib/index.ts | 2 + .../mappings_editor/lib/serializers.ts | 46 +++++++ .../components/mappings_editor/lib/utils.ts | 48 ------- .../mappings_editor/shared_imports.ts | 2 + .../components/mappings_editor/types.ts | 1 + 12 files changed, 237 insertions(+), 114 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 2bf3ef89e99c8d..4ef2b34eede3cd 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -6,10 +6,24 @@ import React, { useEffect } from 'react'; import { EuiButtonEmpty, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useForm, Form, SelectField, UseField, FieldConfig } from '../../../shared_imports'; -import { FIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION } from '../../../constants'; +import { + useForm, + Form, + FormDataProvider, + SelectField, + UseField, + FieldConfig, +} from '../../../shared_imports'; + +import { + DATA_TYPE_DEFINITION, + FIELD_TYPES_OPTIONS, + PARAMETERS_DEFINITION, +} from '../../../constants'; + import { useDispatch } from '../../../mappings_state'; -import { Field, ParameterName } from '../../../types'; +import { fieldSerializer } from '../../../lib'; +import { Field, ParameterName, MainType } from '../../../types'; import { NameParameter } from '../field_parameters'; const formWrapper = (props: any) => ; @@ -22,7 +36,7 @@ interface Props { } export const CreateField = React.memo(({ isCancelable = true }: Props) => { - const { form } = useForm(); + const { form } = useForm({ serializer: fieldSerializer }); const dispatch = useDispatch(); useEffect(() => { @@ -50,42 +64,81 @@ export const CreateField = React.memo(({ isCancelable = true }: Props) => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; - return ( - - - - - - - - - - - + const renderFormFields = (type: MainType) => { + const typeDefinition = DATA_TYPE_DEFINITION[type]; + + return ( + + {/* Field name */} + + + + + {/* Field type */} + + + + {/* Field sub type (if any) */} + {typeDefinition && typeDefinition.subTypes && ( + + ({ + value: subType, + text: subType, + })), + hasNoInitialSelection: false, + }, + }} + /> + + )} + + ); + }; + + const renderFormActions = () => ( + + {isCancelable && ( - - {isCancelable && ( - - Cancel - - )} - - - Add - - - + Cancel + )} + + + Add + + + + ); + + return ( + + + + {({ type }) => { + return {renderFormFields(type)}; + }} + + {renderFormActions()} ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index b005e7ab02d1c6..6f68a4cc034caf 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -14,6 +14,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, + EuiText, EuiCode, } from '@elastic/eui'; @@ -21,6 +22,7 @@ import { useForm, Form, OnFormUpdateArg } from '../../../../shared_imports'; import { OnUpdateHandler } from '../../../../../json_editor'; import { useDispatch } from '../../../../mappings_state'; import { Field, NormalizedField } from '../../../../types'; +import { fieldSerializer, fieldDeserializer } from '../../../../lib'; import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; import { EditFieldHeaderForm } from './edit_field_header_form'; import { FieldSettingsJsonEditor } from './field_settings_json_editor'; @@ -32,7 +34,11 @@ interface Props { } export const EditField = React.memo(({ field }: Props) => { - const { form } = useForm({ defaultValue: field.source }); + const { form } = useForm({ + defaultValue: { ...field.source }, + serializer: fieldSerializer, + deserializer: fieldDeserializer, + }); const dispatch = useDispatch(); const fieldsSettings = useRef[0] | undefined>(undefined); @@ -113,7 +119,7 @@ export const EditField = React.memo(({ field }: Props) => { }; const { - source: { name, type, ...fieldsSettingsDefault }, + source: { name, type, subType, ...fieldsSettingsDefault }, } = field; return ( @@ -140,7 +146,7 @@ export const EditField = React.memo(({ field }: Props) => { FormWrapper={formWrapper} onSubmit={getSubmitForm(updateField)} > - + PARAMETERS_DEFINITION[param].fieldConfig || {}; -export const EditFieldHeaderForm = () => { +interface Props { + defaultValue: { [key: string]: any }; +} + +export const EditFieldHeaderForm = ({ defaultValue }: Props) => { return ( - - - - - - - - + + {formData => { + const selectedDatatype = formData.type as MainType; + const typeDefinition = DATA_TYPE_DEFINITION[selectedDatatype]; + + return ( + + {/* Field name */} + + + + + {/* Field type */} + + + + + {/* Field sub type (if any) */} + {typeDefinition && typeDefinition.subTypes && ( + + ({ + value: type, + text: type, + })), + hasNoInitialSelection: false, + }, + }} + /> + + )} + + ); + }} + ); }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 844f94a4abbf74..6d02ebb0b0818a 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -12,7 +12,6 @@ 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'; interface Props { field: NormalizedField; @@ -39,7 +38,8 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { nestedDepth, isExpanded, } = field; - const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; + const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. + // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; const indent = `${nestedDepth * INDENT_SIZE}px`; const indentChild = `${(nestedDepth + 1) * INDENT_SIZE}px`; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts new file mode 100644 index 00000000000000..081c3ee8c2eaa1 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.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. + */ + +/** + * When we want to set a parameter value to the index "default" in a Select option + * we will use this constant to define it. We will then strip this placeholder value + * and let Elasticsearch handle it. + */ +export const INDEX_DEFAULT = 'index_default'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts index 5686e09e08b43c..e1f71537824e89 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts @@ -11,3 +11,5 @@ export * from './data_types_definition'; export * from './parameters_definition'; export * from './mappings_editor'; + +export * from './default_values'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts index 939ed4364c1fca..f1622dee2cc8b5 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts @@ -5,6 +5,7 @@ */ import { FIELD_TYPES, fieldValidators, fieldFormatters, FieldConfig } from '../shared_imports'; import { ParameterName, Parameter } from '../types'; +import { INDEX_DEFAULT } from '../constants'; const { toInt } = fieldFormatters; const { emptyField, containsCharsField } = fieldValidators; @@ -153,21 +154,21 @@ export const PARAMETERS_DEFINITION: { analyzer: { fieldConfig: { label: 'Analyzer', - defaultValue: 'index_default', + defaultValue: INDEX_DEFAULT, type: FIELD_TYPES.SELECT, }, }, search_analyzer: { fieldConfig: { label: 'Search analyzer', - defaultValue: 'index_default', + defaultValue: INDEX_DEFAULT, type: FIELD_TYPES.SELECT, }, }, search_quote_analyzer: { fieldConfig: { label: 'Search quote analyzer', - defaultValue: 'index_default', + defaultValue: INDEX_DEFAULT, type: FIELD_TYPES.SELECT, }, }, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts index afa3a5b4093cb8..86ca9f645c87c7 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts @@ -6,4 +6,6 @@ export * from './utils'; +export * from './serializers'; + export * from './validators'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts new file mode 100644 index 00000000000000..a1fed90bd55706 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.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 { SerializerFunc } from '../shared_imports'; +import { Field, DataType, MainType, SubType } from '../types'; +import { INDEX_DEFAULT, DATA_TYPE_DEFINITION } from '../constants'; +import { getTypeFromSubType } from './utils'; + +const sanitizeField = (field: Field): Field => + Object.entries(field) + // If a parameter value is "index_default", we remove it + .filter(({ 1: value }) => value !== INDEX_DEFAULT) + .reduce( + (acc, [param, value]) => ({ + ...acc, + [param]: value, + }), + {} as any + ); + +export const fieldSerializer: SerializerFunc = (field: Field) => { + // If a subType is present, use it as type for ES + if ({}.hasOwnProperty.call(field, 'subType')) { + field.type = field.subType as DataType; + delete field.subType; + } + return sanitizeField(field); +}; + +export const fieldDeserializer: SerializerFunc = (field: Field): Field => { + if (!DATA_TYPE_DEFINITION[field.type as MainType]) { + const type = getTypeFromSubType(field.type as SubType); + if (!type) { + throw new Error( + `Property type "${field.type}" not recognized and no subType was found for it.` + ); + } + field.subType = field.type as SubType; + field.type = type; + } + + return field; +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 515e05800d09dc..9dba95d4c3b066 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -331,51 +331,3 @@ export const isStateValid = (state: State): boolean | undefined => }, true as undefined | boolean ); - -// const data = { -// title: { -// type: 'text', -// }, -// address: { -// type: 'object', -// properties: { -// street: { -// type: 'text', -// fields: { -// raw: { -// type: 'keyword', -// }, -// }, -// }, -// geo: { -// type: 'object', -// properties: { -// long: { -// type: 'text', -// }, -// lat: { -// type: 'text', -// }, -// }, -// }, -// }, -// }, -// }; - -// const normalized = normalize(data); - -// const { byId } = normalized; -// let addressField; - -// Object.entries(byId).forEach(([id, normalizedField]) => { -// if (normalizedField.path === 'address.street') { -// addressField = normalizedField; -// } -// }); - -// addressField.source.name = 'changed'; -// const updatedById = updateFieldsPathAfterFieldNameChange(addressField, byId); - -// // console.log(JSON.stringify(normalized, null, 4)); -// console.log(JSON.stringify(addressField, null, 4)); -// console.log(JSON.stringify(updatedById, null, 4)); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts index 812f8f8d58f0a1..d5eb10e074a629 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts @@ -9,11 +9,13 @@ export { UseField, getUseField, Form, + FormDataProvider, FormSchema, FIELD_TYPES, VALIDATION_TYPES, OnFormUpdateArg, ValidationFunc, + SerializerFunc, FieldConfig, } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index cc365e2602f3ab..975537b3b7f5ed 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -96,6 +96,7 @@ export interface Fields { export interface Field { name: string; type: DataType; + subType?: SubType; properties?: { [key: string]: Omit }; fields?: { [key: string]: Omit }; } From d42d9da815166bbd6632e9cadee8f57d333e300e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Thu, 17 Oct 2019 18:42:58 +0200 Subject: [PATCH 19/42] Add click outside handler to automatically submit field create form --- .../document_fields/document_fields.tsx | 8 +- .../document_fields/fields/create_field.tsx | 62 ++++++++--- .../fields/edit_field/edit_field.tsx | 1 - .../fields/fields_list_item.tsx | 13 +-- .../mappings_editor/mappings_state.tsx | 63 ++++++++--- .../components/mappings_editor/reducer.ts | 104 +++++++++--------- .../template_form/steps/step_mappings.tsx | 2 +- 7 files changed, 154 insertions(+), 99 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index 83adfb89ad52b3..dfc2126515d7d0 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -38,13 +38,7 @@ export const DocumentFields = () => { return null; } - return ( -
            -
            - 0} /> -
            -
            - ); + return 0} />; }; const renderAddFieldButton = () => { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 4ef2b34eede3cd..5fe46f6f37d9f4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useEffect } from 'react'; -import { EuiButtonEmpty, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiOutsideClickDetector, +} from '@elastic/eui'; import { useForm, @@ -32,10 +38,11 @@ const getFieldConfig = (param: ParameterName): FieldConfig => PARAMETERS_DEFINITION[param].fieldConfig || {}; interface Props { + paddingLeft?: string; isCancelable?: boolean; } -export const CreateField = React.memo(({ isCancelable = true }: Props) => { +export const CreateField = React.memo(({ paddingLeft, isCancelable = true }: Props) => { const { form } = useForm({ serializer: fieldSerializer }); const dispatch = useDispatch(); @@ -47,7 +54,11 @@ export const CreateField = React.memo(({ isCancelable = true }: Props) => { return subscription.unsubscribe; }, [form]); - const submitForm = async (e?: React.FormEvent) => { + const cancel = () => { + dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + }; + + const submitForm = async (e?: React.FormEvent, exitAfter: boolean = false) => { if (e) { e.preventDefault(); } @@ -57,11 +68,23 @@ export const CreateField = React.memo(({ isCancelable = true }: Props) => { if (isValid) { form.reset(); dispatch({ type: 'field.add', value: data }); + + if (exitAfter) { + cancel(); + } } }; - const cancel = () => { - dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + const onClickOutside = () => { + if (!isCancelable) { + return; + } + const name = form.getFields().name.value as string; + if (name.trim() === '') { + cancel(); + } else { + submitForm(undefined, true); + } }; const renderFormFields = (type: MainType) => { @@ -131,15 +154,26 @@ export const CreateField = React.memo(({ isCancelable = true }: Props) => { ); return ( -
            - - - {({ type }) => { - return {renderFormFields(type)}; + + +
            - {renderFormActions()} - - + > +
            + + + {({ type }) => { + return {renderFormFields(type)}; + }} + + {renderFormActions()} + +
            +
            + +
            ); }); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 6f68a4cc034caf..229602cceabd4a 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -14,7 +14,6 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiText, EuiCode, } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 6d02ebb0b0818a..b45996302095e5 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -66,18 +66,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return null; } - return ( -
            -
            - -
            -
            - ); + return ; }; const renderActionButtons = () => { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx index 9c5834c231da6f..6851614b15c12e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx @@ -6,7 +6,14 @@ import React, { useReducer, useEffect, createContext, useContext } from 'react'; -import { reducer, MappingsConfiguration, MappingsFields, State, Dispatch } from './reducer'; +import { + reducer, + addFieldToState, + MappingsConfiguration, + MappingsFields, + State, + Dispatch, +} from './reducer'; import { Field, FieldsEditor } from './types'; import { normalize, deNormalize, canUseMappingsEditor } from './lib'; @@ -22,7 +29,7 @@ export interface Types { export interface OnUpdateHandlerArg { isValid?: boolean; - getData: () => Mappings; + getData: (isValid: boolean) => Mappings; validate: () => Promise; } @@ -72,23 +79,49 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P useEffect(() => { // console.log('State update', state); + // If we are creating a new field, but haven't entered any name + // it is valid and we can byPass its form validation (that requires a "name" to be defined) + const byPassFieldFormValidation = + state.documentFields.status === 'creatingField' && + state.fieldForm !== undefined && + state.fieldForm.data.raw.name.trim() === ''; + onUpdate({ - getData: () => ({ - ...state.configuration.data.format(), - properties: - // Pull the mappings properties from the current editor - state.documentFields.editor === 'json' - ? state.fieldsJsonEditor.format() - : deNormalize(state.fields), - }), + getData: (isValid: boolean) => { + let nextState = state; + + if ( + state.documentFields.status === 'creatingField' && + isValid && + !byPassFieldFormValidation + ) { + // If the form field is valid and we are creating a new field that has some data + // we automatically add the field to our state. + const fieldFormData = state.fieldForm!.data.format() as Field; + if (Object.keys(fieldFormData).length !== 0) { + nextState = addFieldToState(fieldFormData, state); + dispatch({ type: 'field.add', value: fieldFormData }); + } + } + + return { + ...nextState.configuration.data.format(), + properties: + // Pull the mappings properties from the current editor + nextState.documentFields.editor === 'json' + ? nextState.fieldsJsonEditor.format() + : deNormalize(nextState.fields), + }; + }, validate: async () => { - if (state.fieldForm === undefined) { - return (await state.configuration.validate()) && state.fieldsJsonEditor.isValid; + const promisesToValidate = [state.configuration.validate()]; + + if (state.fieldForm !== undefined && !byPassFieldFormValidation) { + promisesToValidate.push(state.fieldForm.validate()); } - return Promise.all([state.configuration.validate(), state.fieldForm.validate()]).then( - ([isConfigurationValid, isFormFieldValid]) => - isConfigurationValid && isFormFieldValid && state.fieldsJsonEditor.isValid + return Promise.all(promisesToValidate).then( + validationArray => validationArray.every(Boolean) && state.fieldsJsonEditor.isValid ); }, isValid: state.isValid, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index 5f8248f2cbf1a4..0299d9ae97ca33 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -63,27 +63,71 @@ export type Action = export type Dispatch = (action: Action) => void; +export const addFieldToState = (field: Field, state: State): State => { + 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); + const { name } = field; + const path = parentField ? `${parentField.path}.${name}` : name; + + state.fields.byId[id] = { + id, + parentId: fieldToAddFieldTo, + source: field, + path, + nestedDepth, + ...getFieldMeta(field), + }; + + if (parentField) { + const childFields = parentField.childFields || []; + + // Update parent field with new children + state.fields.byId[fieldToAddFieldTo!] = { + ...parentField, + childFields: [...childFields, id], + hasChildFields: true, + isExpanded: true, + }; + } + + return { + ...state, + isValid: isStateValid(state), + fields: { ...state.fields, rootLevelFields, maxNestedDepth }, + }; +}; + export const reducer = (state: State, action: Action): State => { switch (action.type) { case 'configuration.update': { - return { + const nextState = { ...state, - isValid: isStateValid({ - ...state, - configuration: action.value, - }), configuration: action.value, }; + + const isValid = isStateValid(nextState); + nextState.isValid = isValid; + + return nextState; } case 'fieldForm.update': { - return { + const nextState = { ...state, - isValid: isStateValid({ - ...state, - fieldForm: action.value, - }), fieldForm: action.value, }; + + const isValid = isStateValid(nextState); + nextState.isValid = isValid; + + return nextState; } case 'documentField.createField': return { @@ -133,45 +177,7 @@ export const reducer = (state: State, action: Action): State => { }; } 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); - const { name } = action.value; - const path = parentField ? `${parentField.path}.${name}` : name; - - state.fields.byId[id] = { - id, - parentId: fieldToAddFieldTo, - source: action.value, - path, - nestedDepth, - ...getFieldMeta(action.value), - }; - - if (parentField) { - const childFields = parentField.childFields || []; - - // Update parent field with new children - state.fields.byId[fieldToAddFieldTo!] = { - ...parentField, - childFields: [...childFields, id], - hasChildFields: true, - isExpanded: true, - }; - } - - return { - ...state, - isValid: isStateValid(state), - fields: { ...state.fields, rootLevelFields, maxNestedDepth }, - }; + return addFieldToState(action.value, state); } case 'field.remove': { const field = state.fields.byId[action.value]; diff --git a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx index 88481ac52a93ef..34629318771f48 100644 --- a/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/template_form/steps/step_mappings.tsx @@ -29,7 +29,7 @@ export const StepMappings: React.FunctionComponent = ({ onStepValidityChange(isValid); setDataGetter(async () => { const isMappingsValid = isValid === undefined ? await validate() : isValid; - const mappings = getData(); + const mappings = getData(isMappingsValid); return Promise.resolve({ isValid: isMappingsValid, data: { mappings } }); }); }, From a9b078ac0440580a28fe88ad25c106d75d406bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 11:18:55 +0200 Subject: [PATCH 20/42] Tidy styling when the list has only 1 level depth --- .../components/mappings_editor/_index.scss | 35 +++++++++++++++---- .../document_fields/fields/create_field.tsx | 13 ++++--- .../document_fields/fields/fields_list.tsx | 5 ++- .../fields/fields_list_item.tsx | 30 ++++++++++------ .../components/mappings_editor/reducer.ts | 9 ++--- 5 files changed, 65 insertions(+), 27 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 5166892c7f8772..ddcbc2c2911a73 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -1,7 +1,10 @@ /* [1] When the component is embedded inside the tree, we need - to add some extra indent to make room for the child "bullet" on the left. + to add some extra indent to make room for the child "L" bullet on the left. + + [2] By default all content have a padding left to leave some room for the "L" bullet + unless "--toggle" is added. In that case we don't need padding as the toggle will add it. */ .mappings-editor { @@ -32,9 +35,21 @@ } } + &__wrapper { + padding-left: $euiSizeXS; + + &--indent { + padding-left: $euiSize; + } + } + &__content { height: $euiSizeXL * 2; position: relative; + + &--indent { + padding-left: $euiSizeXL; + } } &__toggle { @@ -84,12 +99,18 @@ padding-left: $euiSizeXXL - $euiSizeXS; // [1] } - .mappings-editor__fields-list .mappings-editor__fields-list-item__content--toggle { - &::before{ - content: none; - } - &::after { - content: none; + .mappings-editor__fields-list .mappings-editor__fields-list-item__content { + padding-left: $euiSizeXL; // [2] + + &--toggle { + padding-left: 0; + + &::before{ + content: none; + } + &::after { + content: none; + } } } } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 5fe46f6f37d9f4..1ed5c38142680f 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -42,7 +42,10 @@ interface Props { isCancelable?: boolean; } -export const CreateField = React.memo(({ paddingLeft, isCancelable = true }: Props) => { +export const CreateField = React.memo(function CreateFieldComponent({ + paddingLeft, + isCancelable = true, +}: Props) { const { form } = useForm({ serializer: fieldSerializer }); const dispatch = useDispatch(); @@ -76,12 +79,12 @@ export const CreateField = React.memo(({ paddingLeft, isCancelable = true }: Pro }; const onClickOutside = () => { - if (!isCancelable) { - return; - } const name = form.getFields().name.value as string; + if (name.trim() === '') { - cancel(); + if (isCancelable) { + cancel(); + } } else { submitForm(undefined, true); } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx index a25cd7edacd1b4..c94118ed310960 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -13,7 +13,10 @@ interface Props { treeDepth?: number; } -export const FieldsList = React.memo(({ fields = [], treeDepth = 0 }: Props) => { +export const FieldsList = React.memo(function FieldsListComponent({ + fields = [], + treeDepth = 0, +}: Props) { return (
              {fields.map(field => ( diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index b45996302095e5..e4ced957425b56 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.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, { useMemo } from 'react'; import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiBadge, EuiButtonIcon } from '@elastic/eui'; @@ -24,9 +24,9 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const dispatch = useDispatch(); const { documentFields: { status, fieldToAddFieldTo, fieldToEdit }, - fields: { byId }, + fields: { byId, maxNestedDepth }, } = useState(); - const getField = (propId: string) => byId[propId]; + const getField = (fieldId: string) => byId[fieldId]; const { id, source, @@ -41,7 +41,11 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; const indent = `${nestedDepth * INDENT_SIZE}px`; - const indentChild = `${(nestedDepth + 1) * INDENT_SIZE}px`; + const indentChild = `${(nestedDepth + 1) * INDENT_SIZE + 4}px`; // We need to add 4 because we have a padding left set to 4 on the wrapper + const childFieldsArray = useMemo(() => (hasChildFields ? childFields!.map(getField) : []), [ + childFields, + byId, + ]); const addField = () => { dispatch({ @@ -112,24 +116,30 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { status === 'editingField' && fieldToEdit !== id, })} > -
              +
              nestedDepth, })} > - - {hasChildFields && ( + {hasChildFields && ( + - )} - + + )} {source.name} @@ -149,7 +159,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => {
              {hasChildFields && isExpanded && ( - + )} {renderCreateField()} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index 0299d9ae97ca33..5790c889bcefd1 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -65,9 +65,10 @@ export type Dispatch = (action: Action) => void; export const addFieldToState = (field: Field, state: State): State => { const id = getUniqueId(); + const updatedById = { ...state.fields.byId }; const { fieldToAddFieldTo } = state.documentFields; const addToRootLevel = fieldToAddFieldTo === undefined; - const parentField = addToRootLevel ? undefined : state.fields.byId[fieldToAddFieldTo!]; + const parentField = addToRootLevel ? undefined : updatedById[fieldToAddFieldTo!]; const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, id] @@ -77,7 +78,7 @@ export const addFieldToState = (field: Field, state: State): State => { const { name } = field; const path = parentField ? `${parentField.path}.${name}` : name; - state.fields.byId[id] = { + updatedById[id] = { id, parentId: fieldToAddFieldTo, source: field, @@ -90,7 +91,7 @@ export const addFieldToState = (field: Field, state: State): State => { const childFields = parentField.childFields || []; // Update parent field with new children - state.fields.byId[fieldToAddFieldTo!] = { + updatedById[fieldToAddFieldTo!] = { ...parentField, childFields: [...childFields, id], hasChildFields: true, @@ -101,7 +102,7 @@ export const addFieldToState = (field: Field, state: State): State => { return { ...state, isValid: isStateValid(state), - fields: { ...state.fields, rootLevelFields, maxNestedDepth }, + fields: { ...state.fields, byId: updatedById, rootLevelFields, maxNestedDepth }, }; }; From 69594d96667901615d05dde158fbb0a6a86e378b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 13:09:27 +0200 Subject: [PATCH 21/42] Add multi-field button for "text" and "keyword" --- .../components/mappings_editor/_index.scss | 6 +- .../fields/fields_list_item.tsx | 56 +++++++++++++------ .../constants/parameters_definition.ts | 2 +- .../mappings_editor/mappings_editor.tsx | 1 + .../components/mappings_editor/reducer.ts | 3 +- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index ddcbc2c2911a73..f7011f137440d2 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -14,7 +14,8 @@ &:hover { background-color: $euiColorLightestShade; - .mappings-editor__fields-list-item__actions { + .mappings-editor__fields-list-item__actions, + .mappings-editor__fields-list-item__multi-field-button { opacity: 1; } } @@ -57,7 +58,8 @@ width: $euiSizeL; } - &__actions { + &__actions, + &__multi-field-button { opacity: 0; padding-right: $euiSizeS; transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index e4ced957425b56..be2935f3c8d71f 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -5,7 +5,14 @@ */ import React, { useMemo } from 'react'; import classNames from 'classnames'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiBadge, EuiButtonIcon } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiBadge, + EuiFacetButton, + EuiButtonIcon, +} from '@elastic/eui'; import { useState, useDispatch } from '../../../mappings_state'; import { FieldsList } from './fields_list'; @@ -37,15 +44,18 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { hasMultiFields, nestedDepth, isExpanded, + parentId, } = field; const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; const indent = `${nestedDepth * INDENT_SIZE}px`; const indentChild = `${(nestedDepth + 1) * INDENT_SIZE + 4}px`; // We need to add 4 because we have a padding left set to 4 on the wrapper - const childFieldsArray = useMemo(() => (hasChildFields ? childFields!.map(getField) : []), [ - childFields, - byId, - ]); + const parentField = parentId !== undefined ? byId[parentId] : undefined; + const isMultiField = parentField !== undefined ? parentField.canHaveMultiFields === true : false; + const childFieldsArray = useMemo( + () => (hasChildFields || hasMultiFields ? childFields!.map(getField) : []), + [childFields, byId] + ); const addField = () => { dispatch({ @@ -80,16 +90,11 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { return ( - {(canHaveMultiFields || canHaveChildFields) && ( + {canHaveChildFields && ( - {canHaveChildFields && ( - - Add child - - )} - {canHaveMultiFields && ( - Multi-fields - )} + + Add child + )} @@ -146,11 +151,28 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { {source.type} - {hasMultiFields && ( + {!isMultiField && canHaveMultiFields && ( + <> + {hasMultiFields && ( + + + + + )} + + + Add multi-field + + + + )} + {/* {hasMultiFields && ( {`${childFields!.length} multi-field`} - )} + )} */} {renderActionButtons()} @@ -158,7 +180,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => {
            - {hasChildFields && isExpanded && ( + {Boolean(childFieldsArray.length) && isExpanded && ( )} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts index f1622dee2cc8b5..f32ec8a732b4a8 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.ts @@ -337,7 +337,7 @@ export const PARAMETERS_DEFINITION: { ignore_above: { fieldConfig: { label: 'Ignore above', - defaultValue: 2147483647, + defaultValue: 256, type: FIELD_TYPES.NUMBER, formatters: [toInt], validations: [ diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx index fd097d70a82f23..f747ce51293046 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_editor.tsx @@ -46,6 +46,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue }: Props) => return (
            + {renderEditor()} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index 5790c889bcefd1..c46690a9e3b6ba 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -94,7 +94,8 @@ export const addFieldToState = (field: Field, state: State): State => { updatedById[fieldToAddFieldTo!] = { ...parentField, childFields: [...childFields, id], - hasChildFields: true, + hasChildFields: parentField.canHaveChildFields ? true : false, + hasMultiFields: parentField.canHaveMultiFields ? true : false, isExpanded: true, }; } From 858c60beb22bab02f7110e0658c245b32b168421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 15:37:51 +0200 Subject: [PATCH 22/42] Create stateless component for FieldListItem --- .../forms/hook_form_lib/hooks/use_form.ts | 2 +- .../static/forms/hook_form_lib/lib/subject.ts | 6 +- .../document_fields/fields/create_field.tsx | 6 +- .../document_fields/fields/fields_list.tsx | 15 ++-- .../fields/fields_list_item.tsx | 73 ++++++++----------- .../fields/fields_list_item_container.tsx | 73 +++++++++++++++++++ .../components/mappings_editor/reducer.ts | 23 +++++- 7 files changed, 143 insertions(+), 55 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx 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 9364aa48a47971..3c92a1ed70ba66 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 @@ -118,7 +118,7 @@ export function useForm( if (!areAllFieldsValidated) { // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" - return; + return undefined; } const isFormValid = fieldsArray.every(isFieldValid); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts index 4c0169cb526e2a..7365f234d39ed8 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts @@ -51,7 +51,9 @@ export class Subject { } next(value: T) { - this.value = value; - this.callbacks.forEach(fn => fn(value)); + if (value !== this.value) { + this.value = value; + this.callbacks.forEach(fn => fn(value)); + } } } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 1ed5c38142680f..7c1b46e132fbe7 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -44,7 +44,7 @@ interface Props { export const CreateField = React.memo(function CreateFieldComponent({ paddingLeft, - isCancelable = true, + isCancelable, }: Props) { const { form } = useForm({ serializer: fieldSerializer }); const dispatch = useDispatch(); @@ -82,7 +82,7 @@ export const CreateField = React.memo(function CreateFieldComponent({ const name = form.getFields().name.value as string; if (name.trim() === '') { - if (isCancelable) { + if (isCancelable !== false) { cancel(); } } else { @@ -143,7 +143,7 @@ export const CreateField = React.memo(function CreateFieldComponent({ const renderFormActions = () => ( - {isCancelable && ( + {isCancelable !== false && ( Cancel diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx index c94118ed310960..e2b6263ec4f525 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { FieldsListItem } from './fields_list_item'; +import { FieldsListItemContainer } from './fields_list_item_container'; import { NormalizedField } from '../../../types'; interface Props { @@ -13,15 +13,18 @@ interface Props { treeDepth?: number; } -export const FieldsList = React.memo(function FieldsListComponent({ - fields = [], - treeDepth = 0, -}: Props) { +export const FieldsList = React.memo(function FieldsListComponent({ fields, treeDepth }: Props) { + if (fields === undefined) { + return null; + } return (
              {fields.map(field => (
            • - +
            • ))}
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index be2935f3c8d71f..0f0caf5979ad92 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.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 classNames from 'classnames'; import { EuiFlexGroup, @@ -14,7 +14,6 @@ import { EuiButtonIcon, } 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'; @@ -22,20 +21,36 @@ import { NormalizedField } from '../../../types'; interface Props { field: NormalizedField; - treeDepth?: number; + isCreateFieldFormVisible: boolean; + areActionButtonsVisible: boolean; + isHighlighted: boolean; + isDimmed: boolean; + isMultiField: boolean; + childFieldsArray: NormalizedField[]; + maxNestedDepth: number; + addField(): void; + editField(): void; + toggleExpand(): void; + treeDepth: number; } const INDENT_SIZE = 32; -export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { - const dispatch = useDispatch(); +export const FieldsListItem = React.memo(function FieldListItemComponent({ + field, + isHighlighted, + isDimmed, + isMultiField, + isCreateFieldFormVisible, + areActionButtonsVisible, + childFieldsArray, + maxNestedDepth, + addField, + editField, + toggleExpand, + treeDepth = 0, +}: Props) { const { - documentFields: { status, fieldToAddFieldTo, fieldToEdit }, - fields: { byId, maxNestedDepth }, - } = useState(); - const getField = (fieldId: string) => byId[fieldId]; - const { - id, source, childFields, canHaveChildFields, @@ -44,39 +59,14 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { hasMultiFields, nestedDepth, isExpanded, - parentId, } = field; const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; const indent = `${nestedDepth * INDENT_SIZE}px`; const indentChild = `${(nestedDepth + 1) * INDENT_SIZE + 4}px`; // We need to add 4 because we have a padding left set to 4 on the wrapper - const parentField = parentId !== undefined ? byId[parentId] : undefined; - const isMultiField = parentField !== undefined ? parentField.canHaveMultiFields === true : false; - const childFieldsArray = useMemo( - () => (hasChildFields || hasMultiFields ? childFields!.map(getField) : []), - [childFields, byId] - ); - - const addField = () => { - dispatch({ - type: 'documentField.createField', - value: id, - }); - }; - - const editField = () => { - dispatch({ - type: 'documentField.editField', - value: id, - }); - }; - - const toggleExpand = () => { - dispatch({ type: 'field.toggleExpand', value: { fieldId: id } }); - }; const renderCreateField = () => { - if (status !== 'creatingField' || fieldToAddFieldTo !== id) { + if (!isCreateFieldFormVisible) { return null; } @@ -84,7 +74,7 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => { }; const renderActionButtons = () => { - if (status !== 'idle') { + if (!areActionButtonsVisible) { return null; } @@ -116,9 +106,8 @@ export const FieldsListItem = ({ field, treeDepth = 0 }: Props) => {
            { {renderCreateField()} ); -}; +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx new file mode 100644 index 00000000000000..772c5b9194189f --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -0,0 +1,73 @@ +/* + * 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, useCallback } from 'react'; + +import { useState, useDispatch } from '../../../mappings_state'; +import { NormalizedField } from '../../../types'; +import { FieldsListItem } from './fields_list_item'; + +interface Props { + field: NormalizedField; + treeDepth: number; +} + +export const FieldsListItemContainer = React.memo(({ field, treeDepth }: Props) => { + const dispatch = useDispatch(); + const { + documentFields: { status, fieldToAddFieldTo, fieldToEdit }, + fields: { byId, maxNestedDepth }, + } = useState(); + + const getField = (fieldId: string) => byId[fieldId]; + + const { id, childFields, hasChildFields, hasMultiFields, parentId } = field; + const parentField = parentId !== undefined ? byId[parentId] : undefined; + + const isMultiField = parentField !== undefined ? parentField.canHaveMultiFields === true : false; + const isHighlighted = fieldToEdit === id; + const isDimmed = status === 'editingField' && fieldToEdit !== id; + const isCreateFieldFormVisible = status === 'creatingField' && fieldToAddFieldTo === id; + const areActionButtonsVisible = status === 'idle'; + const childFieldsArray = useMemo( + () => (hasChildFields || hasMultiFields ? childFields!.map(getField) : []), + [childFields] + ); + + const addField = useCallback(() => { + dispatch({ + type: 'documentField.createField', + value: id, + }); + }, [id]); + + const editField = useCallback(() => { + dispatch({ + type: 'documentField.editField', + value: id, + }); + }, [id]); + + const toggleExpand = useCallback(() => { + dispatch({ type: 'field.toggleExpand', value: { fieldId: id } }); + }, [id]); + + return ( + + ); +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index c46690a9e3b6ba..d9d2ee25d8e6b9 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -72,7 +72,7 @@ export const addFieldToState = (field: Field, state: State): State => { const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, id] - : state.fields.rootLevelFields; + : [...state.fields.rootLevelFields]; const nestedDepth = parentField ? parentField.nestedDepth + 1 : 0; const maxNestedDepth = Math.max(state.fields.maxNestedDepth, nestedDepth); const { name } = field; @@ -98,6 +98,14 @@ export const addFieldToState = (field: Field, state: State): State => { hasMultiFields: parentField.canHaveMultiFields ? true : false, isExpanded: true, }; + + // We _also_ need to make a copy of the parent "childFields" + // array to force a re-render in the view. + if (parentField.parentId) { + updatedById[parentField.parentId].childFields = [ + ...updatedById[parentField.parentId].childFields!, + ]; + } } return { @@ -265,6 +273,18 @@ export const reducer = (state: State, action: Action): State => { updatedById[fieldToEdit] = newField; } + // We _also_ need to make a copy of the parent "childFields" + // array to force a re-render in the view. + let rootLevelFields = state.fields.rootLevelFields; + if (newField.parentId) { + updatedById[newField.parentId].childFields = [ + ...updatedById[newField.parentId].childFields!, + ]; + } else { + // No parent, we need to make a copy of the "rootLevelFields" then + rootLevelFields = [...state.fields.rootLevelFields]; + } + return { ...state, isValid: isStateValid(state), @@ -276,6 +296,7 @@ export const reducer = (state: State, action: Action): State => { }, fields: { ...state.fields, + rootLevelFields, byId: updatedById, }, }; From ed0a64d7ea549f859a8f445ca5c9c9a248241cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 16:08:41 +0200 Subject: [PATCH 23/42] Fix toggle regression --- .../fields/fields_list_item.tsx | 12 +++++- .../components/mappings_editor/reducer.ts | 37 +++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 0f0caf5979ad92..30955e16acd0af 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -35,6 +35,7 @@ interface Props { } const INDENT_SIZE = 32; +const WRAPPER_LEFT_PADDING_SIZE = 4; export const FieldsListItem = React.memo(function FieldListItemComponent({ field, @@ -63,14 +64,21 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; const indent = `${nestedDepth * INDENT_SIZE}px`; - const indentChild = `${(nestedDepth + 1) * INDENT_SIZE + 4}px`; // We need to add 4 because we have a padding left set to 4 on the wrapper + + // When there aren't yet any depth (maxNestedDepth === 0), there are no toggle on the left + // we need to compensate and substract + const substractIndentAmount = maxNestedDepth === 0 ? INDENT_SIZE * 0.5 : 0; + + const indentCreateField = `${(nestedDepth + 1) * INDENT_SIZE + + WRAPPER_LEFT_PADDING_SIZE - + substractIndentAmount}px`; const renderCreateField = () => { if (!isCreateFieldFormVisible) { return null; } - return ; + return ; }; const renderActionButtons = () => { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index d9d2ee25d8e6b9..b791be1e95fdaf 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -65,10 +65,9 @@ export type Dispatch = (action: Action) => void; export const addFieldToState = (field: Field, state: State): State => { const id = getUniqueId(); - const updatedById = { ...state.fields.byId }; const { fieldToAddFieldTo } = state.documentFields; const addToRootLevel = fieldToAddFieldTo === undefined; - const parentField = addToRootLevel ? undefined : updatedById[fieldToAddFieldTo!]; + const parentField = addToRootLevel ? undefined : state.fields.byId[fieldToAddFieldTo!]; const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, id] @@ -78,7 +77,7 @@ export const addFieldToState = (field: Field, state: State): State => { const { name } = field; const path = parentField ? `${parentField.path}.${name}` : name; - updatedById[id] = { + state.fields.byId[id] = { id, parentId: fieldToAddFieldTo, source: field, @@ -91,7 +90,7 @@ export const addFieldToState = (field: Field, state: State): State => { const childFields = parentField.childFields || []; // Update parent field with new children - updatedById[fieldToAddFieldTo!] = { + state.fields.byId[fieldToAddFieldTo!] = { ...parentField, childFields: [...childFields, id], hasChildFields: parentField.canHaveChildFields ? true : false, @@ -102,8 +101,8 @@ export const addFieldToState = (field: Field, state: State): State => { // We _also_ need to make a copy of the parent "childFields" // array to force a re-render in the view. if (parentField.parentId) { - updatedById[parentField.parentId].childFields = [ - ...updatedById[parentField.parentId].childFields!, + state.fields.byId[parentField.parentId].childFields = [ + ...state.fields.byId[parentField.parentId].childFields!, ]; } } @@ -111,7 +110,7 @@ export const addFieldToState = (field: Field, state: State): State => { return { ...state, isValid: isStateValid(state), - fields: { ...state.fields, byId: updatedById, rootLevelFields, maxNestedDepth }, + fields: { ...state.fields, rootLevelFields, maxNestedDepth }, }; }; @@ -139,7 +138,7 @@ export const reducer = (state: State, action: Action): State => { return nextState; } - case 'documentField.createField': + case 'documentField.createField': { return { ...state, documentFields: { @@ -148,7 +147,8 @@ export const reducer = (state: State, action: Action): State => { status: 'creatingField', }, }; - case 'documentField.editField': + } + case 'documentField.editField': { return { ...state, documentFields: { @@ -157,6 +157,7 @@ export const reducer = (state: State, action: Action): State => { fieldToEdit: action.value, }, }; + } case 'documentField.changeStatus': const isValid = action.value === 'idle' ? state.configuration.isValid : state.isValid; return { @@ -238,7 +239,8 @@ export const reducer = (state: State, action: Action): State => { newField = { ...newField, ...getFieldMeta(action.value), - hasChildFields: previousField.hasChildFields, // we need to put that back from our previous field + hasChildFields: previousField.hasChildFields, // we need to put this meta back from our previous field + hasMultiFields: previousField.hasMultiFields, // we need to put this meta back from our previous field }; const shouldDeleteChildFields = shouldDeleteChildFieldsAfterTypeChange( @@ -249,6 +251,7 @@ export const reducer = (state: State, action: Action): State => { if (shouldDeleteChildFields) { newField.childFields = undefined; newField.hasChildFields = false; + newField.hasMultiFields = false; if (previousField.childFields) { const allChildFields = getAllChildFields(previousField, state.fields.byId); @@ -260,6 +263,7 @@ export const reducer = (state: State, action: Action): State => { } let updatedById: NormalizedFields['byId']; + const nameHasChanged = newField.source.name !== previousField.source.name; if (nameHasChanged) { @@ -269,7 +273,7 @@ export const reducer = (state: State, action: Action): State => { updatedById = byId; updatedById[fieldToEdit] = { ...newField, path }; } else { - updatedById = { ...state.fields.byId }; + updatedById = state.fields.byId; updatedById[fieldToEdit] = newField; } @@ -310,6 +314,16 @@ export const reducer = (state: State, action: Action): State => { : action.value.isExpanded, }; + const rootLevelFields = updatedField.parentId + ? state.fields.rootLevelFields + : [...state.fields.rootLevelFields]; + + if (updatedField.parentId) { + state.fields.byId[updatedField.parentId].childFields = [ + ...state.fields.byId[updatedField.parentId].childFields!, + ]; + } + return { ...state, fields: { @@ -318,6 +332,7 @@ export const reducer = (state: State, action: Action): State => { ...state.fields.byId, [action.value.fieldId]: updatedField, }, + rootLevelFields, }, }; } From 97b86081d90f19859738f18d204c14dfdba80cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 16:50:42 +0200 Subject: [PATCH 24/42] Fix left indent on multi-field --- .../document_fields/fields/fields_list_item.tsx | 13 +++++++------ .../fields/fields_list_item_container.tsx | 5 ++++- .../public/components/mappings_editor/reducer.ts | 4 +++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 30955e16acd0af..764730ed02fb63 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -49,7 +49,7 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ addField, editField, toggleExpand, - treeDepth = 0, + treeDepth, }: Props) { const { source, @@ -58,18 +58,18 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ hasChildFields, canHaveMultiFields, hasMultiFields, - nestedDepth, isExpanded, } = field; const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; - const indent = `${nestedDepth * INDENT_SIZE}px`; // When there aren't yet any depth (maxNestedDepth === 0), there are no toggle on the left // we need to compensate and substract const substractIndentAmount = maxNestedDepth === 0 ? INDENT_SIZE * 0.5 : 0; - const indentCreateField = `${(nestedDepth + 1) * INDENT_SIZE + + const indent = `${treeDepth * INDENT_SIZE - substractIndentAmount}px`; + + const indentCreateField = `${(treeDepth + 1) * INDENT_SIZE + WRAPPER_LEFT_PADDING_SIZE - substractIndentAmount}px`; @@ -120,7 +120,8 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ >
            nestedDepth, + !hasChildFields && maxNestedDepth > treeDepth, })} > {hasChildFields && ( diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index 772c5b9194189f..7d115166ad0ff6 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -14,7 +14,10 @@ interface Props { treeDepth: number; } -export const FieldsListItemContainer = React.memo(({ field, treeDepth }: Props) => { +export const FieldsListItemContainer = React.memo(function FieldsListItemContainer({ + field, + treeDepth, +}: Props) { const dispatch = useDispatch(); const { documentFields: { status, fieldToAddFieldTo, fieldToEdit }, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index b791be1e95fdaf..f05440e381dc99 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -72,7 +72,8 @@ export const addFieldToState = (field: Field, state: State): State => { const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, id] : [...state.fields.rootLevelFields]; - const nestedDepth = parentField ? parentField.nestedDepth + 1 : 0; + const nestedDepth = + parentField && parentField.canHaveChildFields ? parentField.nestedDepth + 1 : 0; const maxNestedDepth = Math.max(state.fields.maxNestedDepth, nestedDepth); const { name } = field; const path = parentField ? `${parentField.path}.${name}` : name; @@ -199,6 +200,7 @@ export const reducer = (state: State, action: Action): State => { const parentField = state.fields.byId[parentId]; parentField.childFields = parentField.childFields!.filter(childId => childId !== id); parentField.hasChildFields = Boolean(parentField.childFields.length); + parentField.hasMultiFields = Boolean(parentField.childFields.length); } else { // Deleting a root level field rootLevelFields = rootLevelFields.filter(childId => childId !== id); From c9d3c419b0f67e7cd0c212e8516e8cdd4adc335d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 16:58:16 +0200 Subject: [PATCH 25/42] Fix calculate maxNestedDepth --- .../public/components/mappings_editor/lib/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 9dba95d4c3b066..758025eb124fb0 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -159,14 +159,15 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { const { childFieldsName } = meta; if (childFieldsName && field[childFieldsName]) { + const nextDepth = meta.canHaveChildFields ? nestedDepth + 1 : nestedDepth; meta.childFields = []; - maxNestedDepth = Math.max(maxNestedDepth, nestedDepth + 1); + maxNestedDepth = Math.max(maxNestedDepth, nextDepth); normalizeFields( field[meta.childFieldsName!]!, to, [...paths, propName], meta.childFields, - nestedDepth + 1, + nextDepth, id ); } From 07de5cafda8b61f5cedcb081275b1014e6800923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 18 Oct 2019 18:36:57 +0200 Subject: [PATCH 26/42] Add Tree component to display fields + subFields to be deleted --- .../components/mappings_editor/_index.scss | 40 +++++++++++++++++++ .../fields/delete_field_provider.tsx | 28 ++++++------- .../edit_field/update_field_provider.tsx | 19 +++------ .../mappings_editor/components/tree.tsx | 23 +++++++++++ .../mappings_editor/components/tree_item.tsx | 23 +++++++++++ .../components/mappings_editor/lib/utils.ts | 21 ++++++++++ .../components/mappings_editor/types.ts | 5 +++ 7 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.tsx diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index f7011f137440d2..a39c226d12e1ea 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -116,3 +116,43 @@ } } } + +ul.tree { + padding: 0; + margin: 0; + list-style-type: none; + position: relative; + padding-top: $euiSizeXS; + + li.tree-item { + list-style-type: none; + border-left: $euiBorderThin; + margin-left: 1em; + padding-bottom: $euiSizeS; + + div { + padding-left: 1em; + position: relative; + + &::before { + content:''; + position: absolute; + top: 0; + left: -1px; + bottom: 50%; + width: 0.75em; + border: $euiBorderThin; + border-top: none; + border-right: none; + } + } + } + + > li.tree-item:first-child { + padding-top: $euiSizeS; + } + > li.tree-item:last-child { + border-left: 1px solid transparent; + padding-bottom: 0; + } +} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx index f1f2fc02fc5fed..0f29cd76761af7 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx @@ -9,6 +9,8 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { useState as useMappingsState, useDispatch } from '../../../mappings_state'; import { NormalizedField } from '../../../types'; +import { buildFieldTreeFromIds } from '../../../lib'; +import { Tree } from '../../tree'; type DeleteFieldFunc = (property: NormalizedField) => void; @@ -33,9 +35,9 @@ export const DeleteFieldProvider = ({ children }: Props) => { }; const deleteField: DeleteFieldFunc = field => { - const { hasChildFields } = field; + const { hasChildFields, hasMultiFields } = field; - if (hasChildFields) { + if (hasChildFields || hasMultiFields) { setState({ isModalOpen: true, field }); } else { dispatch({ type: 'field.remove', value: field.id }); @@ -49,9 +51,15 @@ export const DeleteFieldProvider = ({ children }: Props) => { const renderModal = () => { const field = state.field!; - const childFields = field.childFields!.map(childId => byId[childId]); const title = `Remove property '${field.source.name}'?`; + // TODO: here indicate if the field is a "multi-field" in a badge + const fieldsTree = buildFieldTreeFromIds( + field.childFields!, + byId, + (_field: NormalizedField) => _field.source.name + ); + return ( { confirmButtonText="Remove" > -

            - 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}
            • - ))} -
            +

            This will also delete the following fields.

            +
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx index fa39a86ea1e8ee..f099264ec282dd 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx @@ -8,8 +8,9 @@ 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 { shouldDeleteChildFieldsAfterTypeChange, buildFieldTreeFromIds } from '../../../../lib'; import { NormalizedField, DataType } from '../../../../types'; +import { Tree } from '../../../tree'; export type UpdateFieldFunc = (field: NormalizedField) => void; @@ -81,7 +82,8 @@ export const UpdateFieldProvider = ({ children }: Props) => { 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]); + + const fieldsTree = buildFieldTreeFromIds(field.childFields!, byId); return ( @@ -94,17 +96,8 @@ export const UpdateFieldProvider = ({ children }: Props) => { confirmButtonText="Confirm type change" > -

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

            -
              - {childFields - .map(_field => _field.source.name) - .sort() - .map(name => ( -
            • {name}
            • - ))} -
            +

            This will delete the following fields.

            +
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.tsx new file mode 100644 index 00000000000000..c7a211a36f7412 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.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 { TreeItem as TreeItemType } from '../types'; +import { TreeItem } from './tree_item'; + +interface Props { + tree: TreeItemType[]; +} + +export const Tree = ({ tree }: Props) => { + return ( +
              + {tree.map(treeItem => ( + + ))} +
            + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.tsx new file mode 100644 index 00000000000000..be87db0ed36b41 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.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 { TreeItem as TreeItemType } from '../types'; +import { Tree } from './tree'; + +interface Props { + treeItem: TreeItemType; +} + +export const TreeItem = ({ treeItem }: Props) => { + return ( +
          • +
            {treeItem.label}
            + {treeItem.children && } +
          • + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 758025eb124fb0..caf7053232296a 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -14,8 +14,11 @@ import { MainType, SubType, ChildFieldName, + TreeItem, } from '../types'; + import { DATA_TYPE_DEFINITION, MAX_DEPTH_DEFAULT_EDITOR } from '../constants'; + import { State } from '../reducer'; export const getUniqueId = () => { @@ -288,6 +291,24 @@ export const getMaxNestedDepth = (byId: NormalizedFields['byId']): number => return Math.max(maxDepth, field.nestedDepth); }, 0); +/** + * Create a nested array of fields and its possible children + * to render a Tree view of them. + */ +export const buildFieldTreeFromIds = ( + fieldsIds: string[], + byId: NormalizedFields['byId'], + render: (field: NormalizedField) => JSX.Element | string +): TreeItem[] => + fieldsIds.map(id => { + const field = byId[id]; + const children = field.childFields + ? buildFieldTreeFromIds(field.childFields, byId, render) + : undefined; + + return { label: render(field), children }; + }); + /** * 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. diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 975537b3b7f5ed..563b47eaf2b113 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -130,3 +130,8 @@ export interface NormalizedField extends FieldMeta { export type ChildFieldName = 'properties' | 'fields'; export type FieldsEditor = 'default' | 'json'; + +export interface TreeItem { + label: string | JSX.Element; + children?: TreeItem[]; +} From 67e4c0db44fddfef27eca85e596ef808fe626715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 21 Oct 2019 10:42:33 +0200 Subject: [PATCH 27/42] Add "isMultiField" flag to NormalizedField --- .../components/document_fields/fields/fields_list_item.tsx | 3 +-- .../document_fields/fields/fields_list_item_container.tsx | 6 +----- .../public/components/mappings_editor/lib/utils.ts | 4 ++++ .../public/components/mappings_editor/reducer.ts | 2 ++ .../public/components/mappings_editor/types.ts | 1 + 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 764730ed02fb63..965b2a48e0d9fe 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -25,7 +25,6 @@ interface Props { areActionButtonsVisible: boolean; isHighlighted: boolean; isDimmed: boolean; - isMultiField: boolean; childFieldsArray: NormalizedField[]; maxNestedDepth: number; addField(): void; @@ -41,7 +40,6 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ field, isHighlighted, isDimmed, - isMultiField, isCreateFieldFormVisible, areActionButtonsVisible, childFieldsArray, @@ -53,6 +51,7 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ }: Props) { const { source, + isMultiField, childFields, canHaveChildFields, hasChildFields, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index 7d115166ad0ff6..0cb923f4e8a3a1 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -26,10 +26,7 @@ export const FieldsListItemContainer = React.memo(function FieldsListItemContain const getField = (fieldId: string) => byId[fieldId]; - const { id, childFields, hasChildFields, hasMultiFields, parentId } = field; - const parentField = parentId !== undefined ? byId[parentId] : undefined; - - const isMultiField = parentField !== undefined ? parentField.canHaveMultiFields === true : false; + const { id, childFields, hasChildFields, hasMultiFields } = field; const isHighlighted = fieldToEdit === id; const isDimmed = status === 'editingField' && fieldToEdit !== id; const isCreateFieldFormVisible = status === 'creatingField' && fieldToAddFieldTo === id; @@ -63,7 +60,6 @@ export const FieldsListItemContainer = React.memo(function FieldsListItemContain treeDepth={treeDepth} isHighlighted={isHighlighted} isDimmed={isDimmed} - isMultiField={isMultiField} isCreateFieldFormVisible={isCreateFieldFormVisible} areActionButtonsVisible={areActionButtonsVisible} childFieldsArray={childFieldsArray} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index caf7053232296a..fd7822eb271c4f 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -152,6 +152,7 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { paths: string[], idsArray: string[], nestedDepth: number, + isMultiField: boolean = false, parentId?: string ): Record => Object.entries(props).reduce((acc, [propName, value]) => { @@ -165,12 +166,14 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { const nextDepth = meta.canHaveChildFields ? nestedDepth + 1 : nestedDepth; meta.childFields = []; maxNestedDepth = Math.max(maxNestedDepth, nextDepth); + normalizeFields( field[meta.childFieldsName!]!, to, [...paths, propName], meta.childFields, nextDepth, + meta.canHaveMultiFields, id ); } @@ -181,6 +184,7 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { id, parentId, nestedDepth, + isMultiField, path: paths.length ? `${paths.join('.')}.${propName}` : propName, source: rest, ...meta, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index f05440e381dc99..3767492c0c0318 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -68,6 +68,7 @@ export const addFieldToState = (field: Field, state: State): State => { const { fieldToAddFieldTo } = state.documentFields; const addToRootLevel = fieldToAddFieldTo === undefined; const parentField = addToRootLevel ? undefined : state.fields.byId[fieldToAddFieldTo!]; + const isMultiField = parentField ? parentField.canHaveMultiFields : false; const rootLevelFields = addToRootLevel ? [...state.fields.rootLevelFields, id] @@ -81,6 +82,7 @@ export const addFieldToState = (field: Field, state: State): State => { state.fields.byId[id] = { id, parentId: fieldToAddFieldTo, + isMultiField, source: field, path, nestedDepth, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 563b47eaf2b113..96bc2851f28822 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -125,6 +125,7 @@ export interface NormalizedField extends FieldMeta { nestedDepth: number; path: string; source: Omit; + isMultiField: boolean; } export type ChildFieldName = 'properties' | 'fields'; From c60c6b9eae4960866e5c48a66377b08b415b4f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Mon, 21 Oct 2019 14:58:34 +0200 Subject: [PATCH 28/42] Update style for multi-fields --- .../components/mappings_editor/_index.scss | 31 ++++-- .../document_fields/fields/create_field.tsx | 54 +++++++--- .../fields/edit_field/edit_field.tsx | 3 +- .../edit_field/edit_field_header_form.tsx | 35 +++--- .../edit_field/update_field_provider.tsx | 12 ++- .../document_fields/fields/fields_list.tsx | 14 +-- .../fields/fields_list_item.tsx | 52 ++++++--- .../fields/fields_list_item_container.tsx | 3 + .../constants/data_types_definition.ts | 101 +++++++++++++++++- .../constants/field_options.ts | 11 +- .../constants/mappings_editor.ts | 6 ++ .../mappings_editor/lib/serializers.ts | 9 +- .../components/mappings_editor/lib/utils.ts | 6 +- .../components/mappings_editor/reducer.ts | 4 + .../components/mappings_editor/types.ts | 1 + 15 files changed, 268 insertions(+), 74 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index a39c226d12e1ea..faab93c63400d8 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -9,6 +9,12 @@ .mappings-editor { &__fields-list-item { + &--dotted-line { + > .mappings-editor__fields-list-item__field { + border-bottom-style: dashed; + } + } + &__field { border-bottom: $euiBorderThin; @@ -97,6 +103,16 @@ } } + .mappings-editor__create-field-wrapper { + &--multi-field { + .mappings-editor__create-field-content { + &::before, &::after { + content: none; + } + } + } + } + .mappings-editor__create-field-content { padding-left: $euiSizeXXL - $euiSizeXS; // [1] } @@ -104,15 +120,18 @@ .mappings-editor__fields-list .mappings-editor__fields-list-item__content { padding-left: $euiSizeXL; // [2] + &--toggle, &--multi-field { + &::before, &::after { + content: none; + } + } + &--toggle { padding-left: 0; + } - &::before{ - content: none; - } - &::after { - content: none; - } + &--multi-field { + padding-left: $euiSizeS; } } } diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 7c1b46e132fbe7..15676c0361c99e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useEffect } from 'react'; +import classNames from 'classnames'; + import { EuiButtonEmpty, EuiButton, EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector, + EuiIcon, } from '@elastic/eui'; import { @@ -22,9 +25,12 @@ import { } from '../../../shared_imports'; import { - DATA_TYPE_DEFINITION, + TYPE_DEFINITION, FIELD_TYPES_OPTIONS, + MULTIFIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION, + LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER, + EUI_SIZE, } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; @@ -38,11 +44,13 @@ const getFieldConfig = (param: ParameterName): FieldConfig => PARAMETERS_DEFINITION[param].fieldConfig || {}; interface Props { - paddingLeft?: string; + isMultiField?: boolean; + paddingLeft?: number; isCancelable?: boolean; } export const CreateField = React.memo(function CreateFieldComponent({ + isMultiField, paddingLeft, isCancelable, }: Props) { @@ -91,7 +99,13 @@ export const CreateField = React.memo(function CreateFieldComponent({ }; const renderFormFields = (type: MainType) => { - const typeDefinition = DATA_TYPE_DEFINITION[type]; + const typeDefinition = TYPE_DEFINITION[type]; + const subTypeOptions = + typeDefinition && typeDefinition.subTypes + ? typeDefinition.subTypes.types + .map(subType => TYPE_DEFINITION[subType]) + .map(subType => ({ value: subType.value, text: subType.label })) + : undefined; return ( @@ -108,29 +122,26 @@ export const CreateField = React.memo(function CreateFieldComponent({ component={SelectField} componentProps={{ euiFieldProps: { - options: FIELD_TYPES_OPTIONS, + options: isMultiField ? MULTIFIELD_TYPES_OPTIONS : FIELD_TYPES_OPTIONS, }, }} /> {/* Field sub type (if any) */} - {typeDefinition && typeDefinition.subTypes && ( + {subTypeOptions && ( ({ - value: subType, - text: subType, - })), + options: subTypeOptions, hasNoInitialSelection: false, }, }} @@ -142,7 +153,7 @@ export const CreateField = React.memo(function CreateFieldComponent({ }; const renderFormActions = () => ( - + {isCancelable !== false && ( Cancel @@ -160,19 +171,30 @@ export const CreateField = React.memo(function CreateFieldComponent({
            - + + {isMultiField && ( + + + + )} {({ type }) => { return {renderFormFields(type)}; }} - {renderFormActions()} + {renderFormActions()}
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 229602cceabd4a..c7a8fe7f535499 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -119,6 +119,7 @@ export const EditField = React.memo(({ field }: Props) => { const { source: { name, type, subType, ...fieldsSettingsDefault }, + isMultiField, } = field; return ( @@ -145,7 +146,7 @@ export const EditField = React.memo(({ field }: Props) => { FormWrapper={formWrapper} onSubmit={getSubmitForm(updateField)} > - + PARAMETERS_DEFINITION[param].fieldConfig || {}; interface Props { - defaultValue: { [key: string]: any }; + isMultiField: boolean; + defaultValue: Field; } -export const EditFieldHeaderForm = ({ defaultValue }: Props) => { +export const EditFieldHeaderForm = ({ isMultiField, defaultValue }: Props) => { return ( {formData => { const selectedDatatype = formData.type as MainType; - const typeDefinition = DATA_TYPE_DEFINITION[selectedDatatype]; + const typeDefinition = TYPE_DEFINITION[selectedDatatype]; + const subTypeOptions = + typeDefinition && typeDefinition.subTypes + ? typeDefinition.subTypes.types + .map(subType => TYPE_DEFINITION[subType]) + .map(subType => ({ value: subType.value, text: subType.label })) + : undefined; return ( @@ -46,7 +54,7 @@ export const EditFieldHeaderForm = ({ defaultValue }: Props) => { component={SelectField} componentProps={{ euiFieldProps: { - options: FIELD_TYPES_OPTIONS, + options: isMultiField ? MULTIFIELD_TYPES_OPTIONS : FIELD_TYPES_OPTIONS, hasNoInitialSelection: true, }, }} @@ -54,22 +62,23 @@ export const EditFieldHeaderForm = ({ defaultValue }: Props) => {
            {/* Field sub type (if any) */} - {typeDefinition && typeDefinition.subTypes && ( + {subTypeOptions && ( ({ - value: type, - text: type, - })), + options: subTypeOptions, hasNoInitialSelection: false, }, }} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx index f099264ec282dd..017f20b04ff706 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx @@ -44,10 +44,10 @@ export const UpdateFieldProvider = ({ children }: Props) => { oldType: DataType, newType: DataType ): { requiresConfirmation: boolean } => { - const { hasChildFields } = field; + const { hasChildFields, hasMultiFields, canHaveChildFields, canHaveMultiFields } = field; - if (!hasChildFields) { - // No child fields will be deleted, no confirmation needed. + if ((!hasChildFields && canHaveChildFields) || (!hasMultiFields && canHaveMultiFields)) { + // No child or multi-fields will be deleted, no confirmation needed. return { requiresConfirmation: false }; } @@ -83,7 +83,11 @@ export const UpdateFieldProvider = ({ children }: Props) => { const field = state.field!; const title = `Confirm change '${field.source.name}' type to "${field.source.type}".`; - const fieldsTree = buildFieldTreeFromIds(field.childFields!, byId); + const fieldsTree = buildFieldTreeFromIds( + field.childFields!, + byId, + (_field: NormalizedField) => _field.source.name + ); return ( diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx index e2b6263ec4f525..311909b12318b7 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -19,13 +19,13 @@ export const FieldsList = React.memo(function FieldsListComponent({ fields, tree } return (
              - {fields.map(field => ( -
            • - -
            • + {fields.map((field, index) => ( + ))}
            ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 965b2a48e0d9fe..8f01dda9d9f88d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -12,12 +12,14 @@ import { EuiBadge, EuiFacetButton, EuiButtonIcon, + EuiIcon, } from '@elastic/eui'; +import { NormalizedField } from '../../../types'; +import { CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER } from '../../../constants'; import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; import { DeleteFieldProvider } from './delete_field_provider'; -import { NormalizedField } from '../../../types'; interface Props { field: NormalizedField; @@ -25,6 +27,7 @@ interface Props { areActionButtonsVisible: boolean; isHighlighted: boolean; isDimmed: boolean; + isLastItem: boolean; childFieldsArray: NormalizedField[]; maxNestedDepth: number; addField(): void; @@ -33,15 +36,13 @@ interface Props { treeDepth: number; } -const INDENT_SIZE = 32; -const WRAPPER_LEFT_PADDING_SIZE = 4; - export const FieldsListItem = React.memo(function FieldListItemComponent({ field, isHighlighted, isDimmed, isCreateFieldFormVisible, areActionButtonsVisible, + isLastItem, childFieldsArray, maxNestedDepth, addField, @@ -62,22 +63,29 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; - // When there aren't yet any depth (maxNestedDepth === 0), there are no toggle on the left - // we need to compensate and substract - const substractIndentAmount = maxNestedDepth === 0 ? INDENT_SIZE * 0.5 : 0; + // When there aren't any "child" fields (the maxNestedDepth === 0), there are no toggle icon on the left of any field. + // For that reason, we need to compensate and substract some indent to left align on the page. + const substractIndentAmount = maxNestedDepth === 0 ? CHILD_FIELD_INDENT_SIZE * 0.5 : 0; - const indent = `${treeDepth * INDENT_SIZE - substractIndentAmount}px`; + const indent = treeDepth * CHILD_FIELD_INDENT_SIZE - substractIndentAmount; - const indentCreateField = `${(treeDepth + 1) * INDENT_SIZE + - WRAPPER_LEFT_PADDING_SIZE - - substractIndentAmount}px`; + const indentCreateField = + (treeDepth + 1) * CHILD_FIELD_INDENT_SIZE + + LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER - + substractIndentAmount; + + const hasDottedLine = isMultiField + ? isLastItem + ? false + : true + : canHaveMultiFields && isExpanded; const renderCreateField = () => { if (!isCreateFieldFormVisible) { return null; } - return ; + return ; }; const renderActionButtons = () => { @@ -109,9 +117,13 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ }; return ( - <> +
          • treeDepth, })} @@ -142,6 +155,11 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ /> )} + {isMultiField && ( + + + + )} {source.name} @@ -152,7 +170,9 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ <> {hasMultiFields && ( - + + + + + )} +
          • ); }); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index 0cb923f4e8a3a1..ab3ff83de0a80d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -12,11 +12,13 @@ import { FieldsListItem } from './fields_list_item'; interface Props { field: NormalizedField; treeDepth: number; + isLastItem: boolean; } export const FieldsListItemContainer = React.memo(function FieldsListItemContainer({ field, treeDepth, + isLastItem, }: Props) { const dispatch = useDispatch(); const { @@ -62,6 +64,7 @@ export const FieldsListItemContainer = React.memo(function FieldsListItemContain isDimmed={isDimmed} isCreateFieldFormVisible={isCreateFieldFormVisible} areActionButtonsVisible={areActionButtonsVisible} + isLastItem={isLastItem} childFieldsArray={childFieldsArray} maxNestedDepth={maxNestedDepth} addField={addField} diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.ts index 0ddced236c0f4f..aa8954867f4eb0 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.ts @@ -4,18 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MainType, DataTypeDefinition } from '../types'; +import { MainType, DataType, DataTypeDefinition } from '../types'; -export const DATA_TYPE_DEFINITION: { [key in MainType]: DataTypeDefinition } = { +export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { text: { + value: 'text', label: 'Text', basicParameters: ['store', 'index', 'fielddata'], }, keyword: { + value: 'keyword', label: 'Keyword', basicParameters: ['store', 'index', 'doc_values'], }, numeric: { + value: 'numeric', label: 'Numeric', subTypes: { label: 'Numeric type', @@ -28,6 +31,7 @@ export const DATA_TYPE_DEFINITION: { [key in MainType]: DataTypeDefinition } = { }, date: { label: 'Date', + value: 'date', subTypes: { label: 'Date type', types: ['date', 'date_nanos'], @@ -39,18 +43,22 @@ export const DATA_TYPE_DEFINITION: { [key in MainType]: DataTypeDefinition } = { }, binary: { label: 'Binary', + value: 'binary', basicParameters: ['doc_values', 'store'], }, ip: { label: 'IP', + value: 'ip', basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], }, boolean: { label: 'Boolean', + value: 'boolean', basicParameters: [['store', 'index', 'doc_values'], ['null_value', 'boost']], }, range: { label: 'Range', + value: 'range', subTypes: { label: 'Range type', types: ['integer_range', 'float_range', 'long_range', 'double_range', 'date_range'], @@ -59,22 +67,111 @@ export const DATA_TYPE_DEFINITION: { [key in MainType]: DataTypeDefinition } = { }, object: { label: 'Object', + value: 'object', basicParameters: ['dynamic', 'enabled'], }, nested: { label: 'Nested', + value: 'nested', basicParameters: ['dynamic'], }, rank_feature: { label: 'Rank feature', + value: 'rank_feature', }, rank_features: { label: 'Rank features', + value: 'date', }, dense_vector: { label: 'Dense vector', + value: 'dense_vector', }, sparse_vector: { label: 'Sparse vector', + value: 'sparse_vector', + }, + byte: { + label: 'Byte', + value: 'byte', + }, + date_nanos: { + label: 'Date nanos', + value: 'date_nanos', + }, + date_range: { + label: 'Date range', + value: 'date_range', + }, + double: { + label: 'Double', + value: 'double', + }, + double_range: { + label: 'Double range', + value: 'double_range', + }, + float: { + label: 'Float', + value: 'float', + }, + float_range: { + label: 'Float range', + value: 'float_range', + }, + half_float: { + label: 'Half float', + value: 'half_float', + }, + integer: { + label: 'Integer', + value: 'integer', + }, + integer_range: { + label: 'Integer range', + value: 'integer_range', + }, + long: { + label: 'Long', + value: 'long', + }, + long_range: { + label: 'Long range', + value: 'long_range', + }, + scaled_float: { + label: 'Scaled float', + value: 'scaled_float', + }, + short: { + label: 'Short', + value: 'short', }, }; + +export const MAIN_TYPES: MainType[] = [ + 'text', + 'keyword', + 'numeric', + 'date', + 'binary', + 'ip', + 'boolean', + 'range', + 'object', + 'nested', + 'rank_feature', + 'rank_features', + 'dense_vector', + 'sparse_vector', +]; + +export const MAIN_DATA_TYPE_DEFINITION: { + [key in MainType]: DataTypeDefinition; +} = MAIN_TYPES.reduce( + (acc, type) => ({ + ...acc, + [type]: TYPE_DEFINITION[type], + }), + {} as { [key in MainType]: DataTypeDefinition } +); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.ts index c60fedcfb12ab7..0fd0ea24663146 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DATA_TYPE_DEFINITION } from './data_types_definition'; +import { MainType } from '../types'; +import { MAIN_DATA_TYPE_DEFINITION } from './data_types_definition'; + +export const TYPE_NOT_ALLOWED_MULTIFIELD: MainType[] = ['object', 'nested']; export const DYNAMIC_SETTING_OPTIONS = [ { value: true, text: 'true' }, @@ -12,9 +15,13 @@ export const DYNAMIC_SETTING_OPTIONS = [ { value: 'strict', text: 'strict' }, ]; -export const FIELD_TYPES_OPTIONS = Object.entries(DATA_TYPE_DEFINITION).map( +export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map( ([dataType, { label }]) => ({ value: dataType, text: label, }) ); + +export const MULTIFIELD_TYPES_OPTIONS = FIELD_TYPES_OPTIONS.filter( + option => TYPE_NOT_ALLOWED_MULTIFIELD.includes(option.value as MainType) === false +); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts index fbb2b18eeb16d0..01cd097807a911 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts @@ -9,3 +9,9 @@ * Above this thresold, the user has to use the JSON editor. */ export const MAX_DEPTH_DEFAULT_EDITOR = 4; + +export const EUI_SIZE = 16; + +export const CHILD_FIELD_INDENT_SIZE = EUI_SIZE * 2; + +export const LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER = EUI_SIZE * 0.25; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts index a1fed90bd55706..409e8bad871b23 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/serializers.ts @@ -6,8 +6,8 @@ import { SerializerFunc } from '../shared_imports'; import { Field, DataType, MainType, SubType } from '../types'; -import { INDEX_DEFAULT, DATA_TYPE_DEFINITION } from '../constants'; -import { getTypeFromSubType } from './utils'; +import { INDEX_DEFAULT, MAIN_DATA_TYPE_DEFINITION } from '../constants'; +import { getMainTypeFromSubType } from './utils'; const sanitizeField = (field: Field): Field => Object.entries(field) @@ -31,8 +31,9 @@ export const fieldSerializer: SerializerFunc = (field: Field) => { }; export const fieldDeserializer: SerializerFunc = (field: Field): Field => { - if (!DATA_TYPE_DEFINITION[field.type as MainType]) { - const type = getTypeFromSubType(field.type as SubType); + if (!MAIN_DATA_TYPE_DEFINITION[field.type as MainType]) { + // IF the type if not one of the main one, it is then probably a "sub" type. + const type = getMainTypeFromSubType(field.type as SubType); if (!type) { throw new Error( `Property type "${field.type}" not recognized and no subType was found for it.` diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index fd7822eb271c4f..0896a7293f40c9 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -17,7 +17,7 @@ import { TreeItem, } from '../types'; -import { DATA_TYPE_DEFINITION, MAX_DEPTH_DEFAULT_EDITOR } from '../constants'; +import { MAIN_DATA_TYPE_DEFINITION, MAX_DEPTH_DEFAULT_EDITOR } from '../constants'; import { State } from '../reducer'; @@ -82,7 +82,7 @@ export const getFieldMeta = (field: Field): FieldMeta => { * short: 'numeric', * } */ -const subTypesMapToType = Object.entries(DATA_TYPE_DEFINITION).reduce( +const subTypesMapToType = Object.entries(MAIN_DATA_TYPE_DEFINITION).reduce( (acc, [type, definition]) => { if ({}.hasOwnProperty.call(definition, 'subTypes')) { definition.subTypes!.types.forEach(subType => { @@ -94,7 +94,7 @@ const subTypesMapToType = Object.entries(DATA_TYPE_DEFINITION).reduce( {} as Record ); -export const getTypeFromSubType = (subType: SubType): MainType => +export const getMainTypeFromSubType = (subType: SubType): MainType => subTypesMapToType[subType] as MainType; /** diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index 3767492c0c0318..88badaa78bf54d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -203,6 +203,10 @@ export const reducer = (state: State, action: Action): State => { parentField.childFields = parentField.childFields!.filter(childId => childId !== id); parentField.hasChildFields = Boolean(parentField.childFields.length); parentField.hasMultiFields = Boolean(parentField.childFields.length); + + if (!parentField.hasChildFields) { + parentField.isExpanded = false; + } } else { // Deleting a root level field rootLevelFields = rootLevelFields.filter(childId => childId !== id); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 96bc2851f28822..236ab3f09230c8 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -7,6 +7,7 @@ import { FieldConfig } from './shared_imports'; export interface DataTypeDefinition { label: string; + value: DataType; subTypes?: { label: string; types: SubType[] }; configuration?: ParameterName[]; basicParameters?: ParameterName[] | ParameterName[][]; From 789136855763cc5d35e8eb32c4131e41ed1ff12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 22 Oct 2019 10:54:12 +0200 Subject: [PATCH 29/42] Display tree of fields to be deleted in modal --- .../document_fields/fields/create_field.tsx | 1 - .../fields/delete_field_provider.tsx | 19 ++-- .../edit_field/edit_field_header_form.tsx | 89 +++++++++++++------ .../edit_field/update_field_provider.tsx | 18 +++- .../components/fields_tree.tsx | 23 +++++ .../mappings_editor/components/tree/index.ts | 7 ++ .../components/{ => tree}/tree.tsx | 14 +-- .../components/{ => tree}/tree_item.tsx | 2 +- .../mappings_editor/shared_imports.ts | 1 + .../components/mappings_editor/types.ts | 5 -- 10 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/index.ts rename x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/{ => tree}/tree.tsx (60%) rename x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/{ => tree}/tree_item.tsx (91%) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 15676c0361c99e..922957d110e33b 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -29,7 +29,6 @@ import { FIELD_TYPES_OPTIONS, MULTIFIELD_TYPES_OPTIONS, PARAMETERS_DEFINITION, - LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER, EUI_SIZE, } from '../../../constants'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx index 0f29cd76761af7..310e928d011a8e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx @@ -5,12 +5,12 @@ */ import React, { useState, Fragment } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiOverlayMask, EuiBadge } from '@elastic/eui'; import { useState as useMappingsState, useDispatch } from '../../../mappings_state'; import { NormalizedField } from '../../../types'; import { buildFieldTreeFromIds } from '../../../lib'; -import { Tree } from '../../tree'; +import { FieldsTree } from '../../fields_tree'; type DeleteFieldFunc = (property: NormalizedField) => void; @@ -53,11 +53,20 @@ export const DeleteFieldProvider = ({ children }: Props) => { const field = state.field!; const title = `Remove property '${field.source.name}'?`; - // TODO: here indicate if the field is a "multi-field" in a badge const fieldsTree = buildFieldTreeFromIds( field.childFields!, byId, - (_field: NormalizedField) => _field.source.name + (fieldItem: NormalizedField) => ( + <> + {fieldItem.source.name} + {fieldItem.isMultiField && ( + <> + {' '} + multi-field + + )} + + ) ); return ( @@ -72,7 +81,7 @@ export const DeleteFieldProvider = ({ children }: Props) => { >

            This will also delete the following fields.

            - +
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index 62aa9ad718660c..44d3b3b1d117f1 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ChangeEvent } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FormDataProvider, SelectField, UseField, FieldConfig } from '../../../../shared_imports'; +import { + FormDataProvider, + SelectField, + UseField, + FieldConfig, + FieldHook, +} from '../../../../shared_imports'; import { MainType, ParameterName, Field } from '../../../../types'; import { TYPE_DEFINITION, @@ -26,17 +32,48 @@ interface Props { } export const EditFieldHeaderForm = ({ isMultiField, defaultValue }: Props) => { + const onTypeChange = (field: FieldHook) => (e: ChangeEvent) => { + const { value } = e.currentTarget; + const typeDefinition = TYPE_DEFINITION[value as MainType]; + const hasSubType = typeDefinition.subTypes !== undefined; + const subTypeField = field.form.getFields().subType; + + if (hasSubType && subTypeField) { + /** + * We need to manually set the subType field value because if we edit a field type that already has a subtype + * (e.g. "numeric" with subType "float"), and we change the type to another one that also has subTypes (e.g. "range"). + * If we then immediately click the "update" button, _without_ changing the subType, the old value of subType + * is maintained. + */ + subTypeField.setValue(typeDefinition.subTypes!.types[0]); + } + + field.setValue(value); + }; + return ( {formData => { - const selectedDatatype = formData.type as MainType; - const typeDefinition = TYPE_DEFINITION[selectedDatatype]; - const subTypeOptions = - typeDefinition && typeDefinition.subTypes - ? typeDefinition.subTypes.types - .map(subType => TYPE_DEFINITION[subType]) - .map(subType => ({ value: subType.value, text: subType.label })) - : undefined; + const type = formData.type as MainType; + + if (!type) { + return null; + } + + const typeDefinition = TYPE_DEFINITION[type]; + const hasSubType = typeDefinition.subTypes !== undefined; + + const subTypeOptions = hasSubType + ? typeDefinition + .subTypes!.types.map(subType => TYPE_DEFINITION[subType]) + .map(subType => ({ value: subType.value, text: subType.label })) + : undefined; + + const defaultValueSubType = hasSubType + ? defaultValue.subType === undefined || type !== defaultValue.type + ? typeDefinition.subTypes!.types[0] + : defaultValue.subType + : undefined; return ( @@ -47,30 +84,26 @@ export const EditFieldHeaderForm = ({ isMultiField, defaultValue }: Props) => { {/* Field type */} - + + {field => ( + + )} + {/* Field sub type (if any) */} - {subTypeOptions && ( + {hasSubType && ( void; @@ -86,7 +86,17 @@ export const UpdateFieldProvider = ({ children }: Props) => { const fieldsTree = buildFieldTreeFromIds( field.childFields!, byId, - (_field: NormalizedField) => _field.source.name + (fieldItem: NormalizedField) => ( + <> + {fieldItem.source.name} + {fieldItem.isMultiField && ( + <> + {' '} + multi-field + + )} + + ) ); return ( @@ -101,7 +111,7 @@ export const UpdateFieldProvider = ({ children }: Props) => { >

            This will delete the following fields.

            - +
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx new file mode 100644 index 00000000000000..6142631c3514f0 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.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 { Tree, TreeItem } from './tree'; + +interface Props { + fields: TreeItem[]; +} + +export const FieldsTree = ({ fields }: Props) => ( +
            +
            +      
            +        
            +      
            +    
            +
            +); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/index.ts new file mode 100644 index 00000000000000..201488a01de94c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/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 './tree'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/tree.tsx similarity index 60% rename from x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.tsx rename to x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/tree.tsx index c7a211a36f7412..35a7ee0e827a81 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/tree.tsx @@ -5,18 +5,22 @@ */ import React from 'react'; -import { TreeItem as TreeItemType } from '../types'; -import { TreeItem } from './tree_item'; +import { TreeItem as TreeItemComponent } from './tree_item'; + +export interface TreeItem { + label: string | JSX.Element; + children?: TreeItem[]; +} interface Props { - tree: TreeItemType[]; + tree: TreeItem[]; } export const Tree = ({ tree }: Props) => { return (
              - {tree.map(treeItem => ( - + {tree.map((treeItem, i) => ( + ))}
            ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/tree_item.tsx similarity index 91% rename from x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.tsx rename to x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/tree_item.tsx index be87db0ed36b41..1c706f26578540 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/tree/tree_item.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { TreeItem as TreeItemType } from '../types'; +import { TreeItem as TreeItemType } from './tree'; import { Tree } from './tree'; interface Props { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts index d5eb10e074a629..7603fa8842c114 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/shared_imports.ts @@ -11,6 +11,7 @@ export { Form, FormDataProvider, FormSchema, + FieldHook, FIELD_TYPES, VALIDATION_TYPES, OnFormUpdateArg, diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 236ab3f09230c8..d77ac0de9ccfd8 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -132,8 +132,3 @@ export interface NormalizedField extends FieldMeta { export type ChildFieldName = 'properties' | 'fields'; export type FieldsEditor = 'default' | 'json'; - -export interface TreeItem { - label: string | JSX.Element; - children?: TreeItem[]; -} From c96bf96d4449ef8bc3f1869b7f03003ecae0b64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 22 Oct 2019 11:08:21 +0200 Subject: [PATCH 30/42] Remove Field settings Json editor --- .../fields/edit_field/edit_field.tsx | 75 +++---------------- .../edit_field/field_settings_json_editor.tsx | 18 ----- 2 files changed, 10 insertions(+), 83 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index c7a8fe7f535499..8be6a42e1c5f2b 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/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, { useRef, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { EuiFlyout, EuiFlyoutHeader, @@ -15,16 +15,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiCode, + EuiSpacer, } from '@elastic/eui'; -import { useForm, Form, OnFormUpdateArg } from '../../../../shared_imports'; -import { OnUpdateHandler } from '../../../../../json_editor'; +import { useForm, Form } from '../../../../shared_imports'; import { useDispatch } from '../../../../mappings_state'; import { Field, NormalizedField } from '../../../../types'; import { fieldSerializer, fieldDeserializer } from '../../../../lib'; import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; import { EditFieldHeaderForm } from './edit_field_header_form'; -import { FieldSettingsJsonEditor } from './field_settings_json_editor'; const formWrapper = (props: any) =>
            ; @@ -40,75 +39,26 @@ export const EditField = React.memo(({ field }: Props) => { }); const dispatch = useDispatch(); - const fieldsSettings = useRef[0] | undefined>(undefined); - const fieldFormUpdate = useRef | undefined>(undefined); - const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { if (e) { e.preventDefault(); } - const { isValid: isFormValid, data: formData } = await form.submit(); - - const { - isValid: isFieldsSettingsValid, - data: { format: getFieldsSettingsData }, - } = fieldsSettings.current!; - - if (isFormValid && isFieldsSettingsValid) { - const fieldsSettingsData = getFieldsSettingsData(); - updateField({ ...field, source: { ...formData, ...fieldsSettingsData } }); - } - }; + const { isValid, data } = await form.submit(); - const getUpdatedField = (): OnFormUpdateArg | void => { - if (fieldFormUpdate.current === undefined || fieldsSettings.current === undefined) { - return; + if (isValid) { + updateField({ ...field, source: data }); } - - const isFormValid = fieldFormUpdate.current.isValid; - const isFieldsSettingsValid = fieldsSettings.current.isValid; - - return { - isValid: isFormValid === undefined ? undefined : isFormValid && isFieldsSettingsValid, - data: { - raw: { ...fieldFormUpdate.current.data.raw, settings: fieldsSettings.current.data.raw }, - format() { - return { - ...fieldFormUpdate.current!.data.format(), - ...fieldsSettings.current!.data.format(), - }; - }, - }, - validate: async () => { - const isFieldFormValid = await fieldFormUpdate.current!.validate(); - return isFieldFormValid && fieldsSettings.current!.isValid; - }, - }; }; useEffect(() => { const subscription = form.subscribe(updatedFieldForm => { - fieldFormUpdate.current = updatedFieldForm; - - const updatedField = getUpdatedField(); - if (updatedField) { - dispatch({ type: 'fieldForm.update', value: updatedField }); - } + dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); }); return subscription.unsubscribe; }, [form]); - const onFieldsSettingsUpdate: OnUpdateHandler = fieldsSettingsUpdate => { - fieldsSettings.current = fieldsSettingsUpdate; - - const updatedField = getUpdatedField(); - if (updatedField) { - dispatch({ type: 'fieldForm.update', value: updatedField }); - } - }; - const exitEdit = () => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); }; @@ -117,10 +67,7 @@ export const EditField = React.memo(({ field }: Props) => { exitEdit(); }; - const { - source: { name, type, subType, ...fieldsSettingsDefault }, - isMultiField, - } = field; + const { isMultiField } = field; return ( @@ -147,11 +94,9 @@ export const EditField = React.memo(({ field }: Props) => { onSubmit={getSubmitForm(updateField)} > + +

            Here will come the form for the parameters....

            - diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx deleted file mode 100644 index b5cd289f1c9b59..00000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/field_settings_json_editor.tsx +++ /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 React from 'react'; - -import { JsonEditor, OnUpdateHandler } from '../../../../../json_editor'; - -interface Props { - onUpdate: OnUpdateHandler; - defaultValue: { [key: string]: any }; -} - -export const FieldSettingsJsonEditor = ({ onUpdate, defaultValue = {} }: Props) => { - return ; -}; From 1eeaf1cb7cc617a8eeaeea1f6ac04ecddcd66350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 22 Oct 2019 11:11:04 +0200 Subject: [PATCH 31/42] Use label for type (in badge) instead of value --- .../document_fields/fields/fields_list_item.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 8f01dda9d9f88d..f9eb6d57a3c8a9 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -16,7 +16,11 @@ import { } from '@elastic/eui'; import { NormalizedField } from '../../../types'; -import { CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER } from '../../../constants'; +import { + TYPE_DEFINITION, + CHILD_FIELD_INDENT_SIZE, + LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER, +} from '../../../constants'; import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; import { DeleteFieldProvider } from './delete_field_provider'; @@ -164,7 +168,7 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ {source.name}
            - {source.type} + {TYPE_DEFINITION[source.type].label} {!isMultiField && canHaveMultiFields && ( <> @@ -185,11 +189,6 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({
            )} - {/* {hasMultiFields && ( - - {`${childFields!.length} multi-field`} - - )} */} {renderActionButtons()} From a5c7e9edc221ab972518d90c68a8bacc8a745daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 22 Oct 2019 13:01:38 +0200 Subject: [PATCH 32/42] Fix tests --- .../public/components/mappings_editor/lib/utils.test.ts | 2 +- .../public/components/mappings_editor/lib/utils.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts index 4aa8aaf7c71a79..0431ea472643b7 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../constants', () => ({ DATA_TYPE_DEFINITION: {} })); +jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} })); import { isStateValid } from './utils'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 0896a7293f40c9..3643ea081aef66 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -14,12 +14,11 @@ import { MainType, SubType, ChildFieldName, - TreeItem, } from '../types'; import { MAIN_DATA_TYPE_DEFINITION, MAX_DEPTH_DEFAULT_EDITOR } from '../constants'; - import { State } from '../reducer'; +import { TreeItem } from '../components/tree'; export const getUniqueId = () => { const dateNow = Date.now(); From 78eb451c4d15263e7a42041dddee4891e5a2058b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 22 Oct 2019 16:18:06 +0200 Subject: [PATCH 33/42] Make CR changes to styling --- .../components/mappings_editor/_index.scss | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index faab93c63400d8..b67eb45bfc181a 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -5,6 +5,9 @@ [2] By default all content have a padding left to leave some room for the "L" bullet unless "--toggle" is added. In that case we don't need padding as the toggle will add it. + + [3] We need to compensate from the -4px margin added by the euiFlexGroup to make sure that the + border-bottom is always visible, even when mouseovering and changing the background color. */ .mappings-editor { @@ -17,6 +20,7 @@ &__field { border-bottom: $euiBorderThin; + margin-top: 4px; // [3] &:hover { background-color: $euiColorLightestShade; @@ -73,20 +77,34 @@ } &__create-field-wrapper { - background-color: $euiColorLightShade; + background-color: $euiColorLightestShade; + border-right: $euiBorderThin; + border-bottom: $euiBorderThin; + border-left: $euiBorderThin; padding: $euiSize; } &__create-field-content { position: relative; } + + &__edit-field { + &__section { + border-bottom: $euiBorderThin; + padding: $euiSizeXL 0; + + &:first-child { + padding-top: 0; + } + } + } } .mappings-editor__fields-list { .mappings-editor__fields-list .mappings-editor__fields-list-item__content, .mappings-editor__create-field-content { &::before { - border-bottom: 1px solid #333; + border-bottom: $euiBorderThin; content: ''; left: $euiSize; position: absolute; @@ -94,7 +112,7 @@ width: $euiSizeS; } &::after { - border-left: 1px solid #333; + border-left: $euiBorderThin; content: ''; left: $euiSize; position: absolute; @@ -146,11 +164,11 @@ ul.tree { li.tree-item { list-style-type: none; border-left: $euiBorderThin; - margin-left: 1em; + margin-left: $euiSizeL; padding-bottom: $euiSizeS; div { - padding-left: 1em; + padding-left: $euiSizeL; position: relative; &::before { @@ -159,7 +177,7 @@ ul.tree { top: 0; left: -1px; bottom: 50%; - width: 0.75em; + width: $euiSize; border: $euiBorderThin; border-top: none; border-right: none; @@ -171,7 +189,7 @@ ul.tree { padding-top: $euiSizeS; } > li.tree-item:last-child { - border-left: 1px solid transparent; + border-left-color: transparent; padding-bottom: 0; } } From 092f5224e4f5332fc5dbdfc3889f3016b83d0cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Tue, 22 Oct 2019 18:35:39 +0200 Subject: [PATCH 34/42] Add CR style change --- .../public/components/mappings_editor/_index.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index b67eb45bfc181a..854a189d022040 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -104,7 +104,7 @@ .mappings-editor__fields-list .mappings-editor__fields-list-item__content, .mappings-editor__create-field-content { &::before { - border-bottom: $euiBorderThin; + border-bottom: 1px solid $euiColorMediumShade; content: ''; left: $euiSize; position: absolute; @@ -112,7 +112,7 @@ width: $euiSizeS; } &::after { - border-left: $euiBorderThin; + border-left: 1px solid $euiColorMediumShade; content: ''; left: $euiSize; position: absolute; From 88624ebb724436167b53281342a82cbfb411fa6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 23 Oct 2019 11:10:03 +0200 Subject: [PATCH 35/42] Increase flyout width from 400 to 720 --- .../components/document_fields/fields/edit_field/edit_field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 8be6a42e1c5f2b..27ba783a081124 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -77,7 +77,7 @@ export const EditField = React.memo(({ field }: Props) => { onClose={exitEdit} aria-labelledby="mappingsEditorFieldEditTitle" size="m" - maxWidth={400} + maxWidth={720} > From b8a199eb78d4d1f1717bcc843898019cd49863b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 23 Oct 2019 11:10:56 +0200 Subject: [PATCH 36/42] Hide "Add multi-field" button when in create or edit mode --- .../fields/fields_list_item.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index f9eb6d57a3c8a9..af94c7805cc801 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -179,14 +179,16 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({
            )} - - - Add multi-field - - + {areActionButtonsVisible && ( + + + Add multi-field + + + )} )} From 25b006ca30b9c8dc4d83d54d7e7baf46a9605d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 23 Oct 2019 11:11:19 +0200 Subject: [PATCH 37/42] Refactor reducer for edit field --- .../components/mappings_editor/reducer.ts | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index 88badaa78bf54d..7dd7c7ff889d5c 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -240,26 +240,28 @@ export const reducer = (state: State, action: Action): State => { source: action.value, }; - if (newField.source.type !== previousField.source.type) { + const nameHasChanged = newField.source.name !== previousField.source.name; + const typeHasChanged = newField.source.type !== previousField.source.type; + + if (typeHasChanged) { // The field `type` has changed, we need to update its meta information // and delete all its children fields. - newField = { - ...newField, - ...getFieldMeta(action.value), - hasChildFields: previousField.hasChildFields, // we need to put this meta back from our previous field - hasMultiFields: previousField.hasMultiFields, // we need to put this meta back from our previous field - }; - const shouldDeleteChildFields = shouldDeleteChildFieldsAfterTypeChange( previousField.source.type, newField.source.type ); + newField = { + ...newField, + ...getFieldMeta(action.value), + hasChildFields: shouldDeleteChildFields ? false : previousField.hasChildFields, + hasMultiFields: shouldDeleteChildFields ? false : previousField.hasMultiFields, + isExpanded: previousField.isExpanded, + }; + if (shouldDeleteChildFields) { newField.childFields = undefined; - newField.hasChildFields = false; - newField.hasMultiFields = false; if (previousField.childFields) { const allChildFields = getAllChildFields(previousField, state.fields.byId); @@ -270,31 +272,25 @@ export const reducer = (state: State, action: Action): State => { } } - let updatedById: NormalizedFields['byId']; - - const nameHasChanged = newField.source.name !== previousField.source.name; - if (nameHasChanged) { // If the name has changed, we need to update the `path` of the field and recursively // the paths of all its "descendant" fields (child or multi-field) const { path, byId } = updateFieldsPathAfterFieldNameChange(newField, state.fields.byId); - updatedById = byId; - updatedById[fieldToEdit] = { ...newField, path }; - } else { - updatedById = state.fields.byId; - updatedById[fieldToEdit] = newField; + newField.path = path; + state.fields.byId = byId; } + state.fields.byId[fieldToEdit] = newField; + // We _also_ need to make a copy of the parent "childFields" // array to force a re-render in the view. - let rootLevelFields = state.fields.rootLevelFields; - if (newField.parentId) { - updatedById[newField.parentId].childFields = [ - ...updatedById[newField.parentId].childFields!, - ]; - } else { - // No parent, we need to make a copy of the "rootLevelFields" then - rootLevelFields = [...state.fields.rootLevelFields]; + const { parentId } = newField; + const rootLevelFields = parentId + ? state.fields.rootLevelFields + : [...state.fields.rootLevelFields]; // No parent, we need to make a copy of the "rootLevelFields" + + if (parentId) { + state.fields.byId[parentId].childFields = [...state.fields.byId[parentId].childFields!]; } return { @@ -309,7 +305,6 @@ export const reducer = (state: State, action: Action): State => { fields: { ...state.fields, rootLevelFields, - byId: updatedById, }, }; } From b839f2a5f8c2a4ba93d08f5fdf85d565c6e90d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 23 Oct 2019 12:17:58 +0200 Subject: [PATCH 38/42] Add toggle also to multi-field --- .../components/mappings_editor/_index.scss | 8 ++++- .../document_fields/fields/create_field.tsx | 4 +++ .../fields/fields_list_item.tsx | 25 ++++++++++------ .../components/mappings_editor/lib/utils.ts | 29 ++++++++++--------- .../components/mappings_editor/reducer.ts | 14 +++++---- 5 files changed, 52 insertions(+), 28 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss index 854a189d022040..10bb08b484706d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss @@ -129,10 +129,16 @@ } } } + + &--toggle { + .mappings-editor__create-field-content { + padding-left: $euiSizeXXL - $euiSizeXS; // [1] + } + } } .mappings-editor__create-field-content { - padding-left: $euiSizeXXL - $euiSizeXS; // [1] + padding-left: $euiSize } .mappings-editor__fields-list .mappings-editor__fields-list-item__content { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx index 922957d110e33b..2171b56dee22bf 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field.tsx @@ -46,12 +46,14 @@ interface Props { isMultiField?: boolean; paddingLeft?: number; isCancelable?: boolean; + maxNestedDepth?: number; } export const CreateField = React.memo(function CreateFieldComponent({ isMultiField, paddingLeft, isCancelable, + maxNestedDepth, }: Props) { const { form } = useForm({ serializer: fieldSerializer }); const dispatch = useDispatch(); @@ -171,6 +173,8 @@ export const CreateField = React.memo(function CreateFieldComponent({
            0, 'mappings-editor__create-field-wrapper--multi-field': isMultiField, })} style={{ diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index af94c7805cc801..a738aa83480ce1 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -10,7 +10,7 @@ import { EuiFlexItem, EuiButtonEmpty, EuiBadge, - EuiFacetButton, + EuiNotificationBadge, EuiButtonIcon, EuiIcon, } from '@elastic/eui'; @@ -89,7 +89,13 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ return null; } - return ; + return ( + + ); }; const renderActionButtons = () => { @@ -143,13 +149,14 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ gutterSize="s" alignItems="center" className={classNames('mappings-editor__fields-list-item__content', { - 'mappings-editor__fields-list-item__content--toggle': hasChildFields, + 'mappings-editor__fields-list-item__content--toggle': + hasChildFields || hasMultiFields, 'mappings-editor__fields-list-item__content--multi-field': isMultiField, 'mappings-editor__fields-list-item__content--indent': - !hasChildFields && maxNestedDepth > treeDepth, + !hasChildFields && !hasMultiFields && maxNestedDepth > treeDepth, })} > - {hasChildFields && ( + {(hasChildFields || hasMultiFields) && ( {TYPE_DEFINITION[source.type].label} - {!isMultiField && canHaveMultiFields && ( + {canHaveMultiFields && ( <> {hasMultiFields && ( - - + - + + {childFields!.length} + )} {areActionButtonsVisible && ( diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index 3643ea081aef66..2c07bcb8cd5f39 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -45,20 +45,22 @@ const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { return undefined; }; -export const getFieldMeta = (field: Field): FieldMeta => { +export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => { const childFieldsName = getChildFieldsName(field.type); - const canHaveChildFields = childFieldsName === 'properties'; - const hasChildFields = - canHaveChildFields && - Boolean(field[childFieldsName!]) && - Object.keys(field[childFieldsName!]!).length > 0; + const canHaveChildFields = isMultiField ? false : childFieldsName === 'properties'; + const hasChildFields = isMultiField + ? false + : canHaveChildFields && + Boolean(field[childFieldsName!]) && + Object.keys(field[childFieldsName!]!).length > 0; - const canHaveMultiFields = childFieldsName === 'fields'; - const hasMultiFields = - canHaveMultiFields && - Boolean(field[childFieldsName!]) && - Object.keys(field[childFieldsName!]!).length > 0; + const canHaveMultiFields = isMultiField ? false : childFieldsName === 'fields'; + const hasMultiFields = isMultiField + ? false + : canHaveMultiFields && + Boolean(field[childFieldsName!]) && + Object.keys(field[childFieldsName!]!).length > 0; return { childFieldsName, @@ -158,11 +160,12 @@ export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { const id = getUniqueId(); idsArray.push(id); const field = { name: propName, ...value } as Field; - const meta = getFieldMeta(field); + const meta = getFieldMeta(field, isMultiField); const { childFieldsName } = meta; if (childFieldsName && field[childFieldsName]) { - const nextDepth = meta.canHaveChildFields ? nestedDepth + 1 : nestedDepth; + const nextDepth = + meta.canHaveChildFields || meta.canHaveMultiFields ? nestedDepth + 1 : nestedDepth; meta.childFields = []; maxNestedDepth = Math.max(maxNestedDepth, nextDepth); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts index 7dd7c7ff889d5c..94fcbcd0fe4b51 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts @@ -74,7 +74,9 @@ export const addFieldToState = (field: Field, state: State): State => { ? [...state.fields.rootLevelFields, id] : [...state.fields.rootLevelFields]; const nestedDepth = - parentField && parentField.canHaveChildFields ? parentField.nestedDepth + 1 : 0; + parentField && (parentField.canHaveChildFields || parentField.canHaveMultiFields) + ? parentField.nestedDepth + 1 + : 0; const maxNestedDepth = Math.max(state.fields.maxNestedDepth, nestedDepth); const { name } = field; const path = parentField ? `${parentField.path}.${name}` : name; @@ -86,7 +88,7 @@ export const addFieldToState = (field: Field, state: State): State => { source: field, path, nestedDepth, - ...getFieldMeta(field), + ...getFieldMeta(field, isMultiField), }; if (parentField) { @@ -195,8 +197,10 @@ export const reducer = (state: State, action: Action): State => { } case 'field.remove': { const field = state.fields.byId[action.value]; - const { id, parentId, hasChildFields } = field; + const { id, parentId, hasChildFields, hasMultiFields } = field; + let { rootLevelFields } = state.fields; + if (parentId) { // Deleting a child field const parentField = state.fields.byId[parentId]; @@ -212,7 +216,7 @@ export const reducer = (state: State, action: Action): State => { rootLevelFields = rootLevelFields.filter(childId => childId !== id); } - if (hasChildFields) { + if (hasChildFields || hasMultiFields) { const allChildFields = getAllChildFields(field, state.fields.byId); allChildFields!.forEach(childField => { delete state.fields.byId[childField.id]; @@ -254,7 +258,7 @@ export const reducer = (state: State, action: Action): State => { newField = { ...newField, - ...getFieldMeta(action.value), + ...getFieldMeta(action.value, newField.isMultiField), hasChildFields: shouldDeleteChildFields ? false : previousField.hasChildFields, hasMultiFields: shouldDeleteChildFields ? false : previousField.hasMultiFields, isExpanded: previousField.isExpanded, From 9de4fafc821c9539aed54d46104d908b1dbd51cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 23 Oct 2019 14:50:29 +0200 Subject: [PATCH 39/42] Refactor: rename "useState" --> "useMappingsState" --- .../components/document_fields/document_fields.tsx | 4 ++-- .../components/document_fields/editor_toggle_controls.tsx | 4 ++-- .../document_fields/field_parameters/name_parameter.tsx | 4 ++-- .../document_fields/fields/delete_field_provider.tsx | 2 +- .../fields/edit_field/update_field_provider.tsx | 2 +- .../document_fields/fields/fields_list_item_container.tsx | 4 ++-- .../public/components/mappings_editor/mappings_state.tsx | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index dfc2126515d7d0..92cdb5b77a9d0f 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -7,7 +7,7 @@ import React, { useEffect } from 'react'; import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; -import { useState, useDispatch } from '../../mappings_state'; +import { useMappingsState, useDispatch } from '../../mappings_state'; import { FieldsList, CreateField, EditField } from './fields'; export const DocumentFields = () => { @@ -15,7 +15,7 @@ export const DocumentFields = () => { const { fields: { byId, rootLevelFields }, documentFields: { status, fieldToAddFieldTo, fieldToEdit }, - } = useState(); + } = useMappingsState(); const getField = (fieldId: string) => byId[fieldId]; const fields = rootLevelFields.map(getField); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx index d1e97386171050..51f9ca63be403d 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton, EuiText } from '@elastic/eui'; -import { useDispatch, useState } from '../../mappings_state'; +import { useDispatch, useMappingsState } from '../../mappings_state'; import { FieldsEditor } from '../../types'; import { canUseMappingsEditor, normalize } from '../../lib'; @@ -18,7 +18,7 @@ interface Props { /* TODO: Review toggle controls UI */ export const EditorToggleControls = ({ editor }: Props) => { const dispatch = useDispatch(); - const { fieldsJsonEditor } = useState(); + const { fieldsJsonEditor } = useMappingsState(); const [showMaxDepthWarning, setShowMaxDepthWarning] = React.useState(false); const [showValidityWarning, setShowValidityWarning] = React.useState(false); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx index 51a20f3e94bb92..397edaf39a6497 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { TextField, UseField, FieldConfig } from '../../../shared_imports'; import { validateUniqueName } from '../../../lib'; import { PARAMETERS_DEFINITION } from '../../../constants'; -import { useState } from '../../../mappings_state'; +import { useMappingsState } from '../../../mappings_state'; export const NameParameter = () => { const { fields: { rootLevelFields, byId }, documentFields: { fieldToAddFieldTo, fieldToEdit }, - } = useState(); + } = useMappingsState(); const { validations, ...rest } = PARAMETERS_DEFINITION.name.fieldConfig as FieldConfig; const initialName = fieldToEdit ? byId[fieldToEdit].source.name : undefined; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx index 310e928d011a8e..19fb2d1ebb3748 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx @@ -7,7 +7,7 @@ import React, { useState, Fragment } from 'react'; import { EuiConfirmModal, EuiOverlayMask, EuiBadge } from '@elastic/eui'; -import { useState as useMappingsState, useDispatch } from '../../../mappings_state'; +import { useMappingsState, useDispatch } from '../../../mappings_state'; import { NormalizedField } from '../../../types'; import { buildFieldTreeFromIds } from '../../../lib'; import { FieldsTree } from '../../fields_tree'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx index 81445f45a58c60..007a043e3eec79 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx @@ -7,7 +7,7 @@ import React, { useState, Fragment } from 'react'; import { EuiConfirmModal, EuiOverlayMask, EuiBadge } from '@elastic/eui'; -import { useState as useMappingsState, useDispatch } from '../../../../mappings_state'; +import { useMappingsState, useDispatch } from '../../../../mappings_state'; import { shouldDeleteChildFieldsAfterTypeChange, buildFieldTreeFromIds } from '../../../../lib'; import { NormalizedField, DataType } from '../../../../types'; import { FieldsTree } from '../../../fields_tree'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index ab3ff83de0a80d..f3ad9cf9b75654 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -5,7 +5,7 @@ */ import React, { useMemo, useCallback } from 'react'; -import { useState, useDispatch } from '../../../mappings_state'; +import { useMappingsState, useDispatch } from '../../../mappings_state'; import { NormalizedField } from '../../../types'; import { FieldsListItem } from './fields_list_item'; @@ -24,7 +24,7 @@ export const FieldsListItemContainer = React.memo(function FieldsListItemContain const { documentFields: { status, fieldToAddFieldTo, fieldToEdit }, fields: { byId, maxNestedDepth }, - } = useState(); + } = useMappingsState(); const getField = (fieldId: string) => byId[fieldId]; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx index 6851614b15c12e..91b5361cb7face 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/mappings_state.tsx @@ -140,10 +140,10 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P ); }); -export const useState = () => { +export const useMappingsState = () => { const ctx = useContext(StateContext); if (ctx === undefined) { - throw new Error('useState must be used within a '); + throw new Error('useMappingsState must be used within a '); } return ctx; }; From 9a00d1166150406509509c09d68cc7e92b1c8e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Wed, 23 Oct 2019 16:50:34 +0200 Subject: [PATCH 40/42] Make CR changes --- .../document_fields/document_fields.tsx | 4 +- .../document_fields/fields/fields_list.tsx | 2 +- .../fields/fields_list_item.tsx | 2 +- .../fields/fields_list_item_container.tsx | 13 ++-- .../components/fields_tree.tsx | 6 ++ .../constants/mappings_editor.ts | 4 ++ .../components/mappings_editor/lib/utils.ts | 15 +--- .../components/mappings_editor/reducer.ts | 72 ++++++------------- 8 files changed, 44 insertions(+), 74 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx index 92cdb5b77a9d0f..208e565c137b85 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/document_fields.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { useMappingsState, useDispatch } from '../../mappings_state'; @@ -18,7 +18,7 @@ export const DocumentFields = () => { } = useMappingsState(); const getField = (fieldId: string) => byId[fieldId]; - const fields = rootLevelFields.map(getField); + const fields = useMemo(() => rootLevelFields.map(getField), [rootLevelFields]); const addField = () => { dispatch({ type: 'documentField.createField' }); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx index 311909b12318b7..80fa890cfa4062 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list.tsx @@ -22,7 +22,7 @@ export const FieldsList = React.memo(function FieldsListComponent({ fields, tree {fields.map((field, index) => ( diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index a738aa83480ce1..c56abe11b5aebd 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -67,7 +67,7 @@ export const FieldsListItem = React.memo(function FieldListItemComponent({ const isAddFieldBtnDisabled = false; // For now, we never disable the Add Child button. // const isAddFieldBtnDisabled = field.nestedDepth === MAX_DEPTH_DEFAULT_EDITOR - 1; - // When there aren't any "child" fields (the maxNestedDepth === 0), there are no toggle icon on the left of any field. + // When there aren't any "child" fields (the maxNestedDepth === 0), there is no toggle icon on the left of any field. // For that reason, we need to compensate and substract some indent to left align on the page. const substractIndentAmount = maxNestedDepth === 0 ? CHILD_FIELD_INDENT_SIZE * 0.5 : 0; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index f3ad9cf9b75654..a1ac25d7f80996 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -10,24 +10,21 @@ import { NormalizedField } from '../../../types'; import { FieldsListItem } from './fields_list_item'; interface Props { - field: NormalizedField; + fieldId: string; treeDepth: number; isLastItem: boolean; } -export const FieldsListItemContainer = React.memo(function FieldsListItemContainer({ - field, - treeDepth, - isLastItem, -}: Props) { +export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Props) => { const dispatch = useDispatch(); const { documentFields: { status, fieldToAddFieldTo, fieldToEdit }, fields: { byId, maxNestedDepth }, } = useMappingsState(); - const getField = (fieldId: string) => byId[fieldId]; + const getField = (id: string) => byId[id]; + const field: NormalizedField = getField(fieldId); const { id, childFields, hasChildFields, hasMultiFields } = field; const isHighlighted = fieldToEdit === id; const isDimmed = status === 'editingField' && fieldToEdit !== id; @@ -72,4 +69,4 @@ export const FieldsListItemContainer = React.memo(function FieldsListItemContain toggleExpand={toggleExpand} /> ); -}); +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx index 6142631c3514f0..8e57ea45f2a32c 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/fields_tree.tsx @@ -12,6 +12,12 @@ interface Props { fields: TreeItem[]; } +/** + * The component expect the children provided to be a string (html). For that reason we can't use it directly with our + * Tree component that renders DOM nodes. + * + * TODO: Open PR on eui repo to allow both string and React.Node to be passed as children of + */ export const FieldsTree = ({ fields }: Props) => (
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts
            index 01cd097807a911..8d851fe10b8cef 100644
            --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts
            +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts
            @@ -10,6 +10,10 @@
              */
             export const MAX_DEPTH_DEFAULT_EDITOR = 4;
             
            +/**
            + * 16px is the default $euiSize Sass variable.
            + * @link https://elastic.github.io/eui/#/guidelines/sass
            + */
             export const EUI_SIZE = 16;
             
             export const CHILD_FIELD_INDENT_SIZE = EUI_SIZE * 2;
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts
            index 2c07bcb8cd5f39..5891b250adc7bd 100644
            --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts
            +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.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 uuid from 'uuid';
             
             import {
               DataType,
            @@ -21,19 +22,7 @@ import { State } from '../reducer';
             import { TreeItem } from '../components/tree';
             
             export const getUniqueId = () => {
            -  const dateNow = Date.now();
            -
            -  let performanceNow: number;
            -  try {
            -    performanceNow = performance.now(); // only available in the browser
            -  } catch {
            -    performanceNow = process.hrtime()[0]; // only in Node
            -  }
            -
            -  return (
            -    '_' +
            -    (Number(String(Math.random()).slice(2)) + dateNow + Math.round(performanceNow)).toString(36)
            -  );
            +  return uuid.v4();
             };
             
             const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => {
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts
            index 94fcbcd0fe4b51..ecb6eaf77b9cae 100644
            --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts
            +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/reducer.ts
            @@ -81,7 +81,7 @@ export const addFieldToState = (field: Field, state: State): State => {
               const { name } = field;
               const path = parentField ? `${parentField.path}.${name}` : name;
             
            -  state.fields.byId[id] = {
            +  const newField: NormalizedField = {
                 id,
                 parentId: fieldToAddFieldTo,
                 isMultiField,
            @@ -91,6 +91,8 @@ export const addFieldToState = (field: Field, state: State): State => {
                 ...getFieldMeta(field, isMultiField),
               };
             
            +  state.fields.byId[id] = newField;
            +
               if (parentField) {
                 const childFields = parentField.childFields || [];
             
            @@ -98,18 +100,10 @@ export const addFieldToState = (field: Field, state: State): State => {
                 state.fields.byId[fieldToAddFieldTo!] = {
                   ...parentField,
                   childFields: [...childFields, id],
            -      hasChildFields: parentField.canHaveChildFields ? true : false,
            -      hasMultiFields: parentField.canHaveMultiFields ? true : false,
            +      hasChildFields: parentField.canHaveChildFields,
            +      hasMultiFields: parentField.canHaveMultiFields,
                   isExpanded: true,
                 };
            -
            -    // We _also_ need to make a copy of the parent "childFields"
            -    // array to force a re-render in the view.
            -    if (parentField.parentId) {
            -      state.fields.byId[parentField.parentId].childFields = [
            -        ...state.fields.byId[parentField.parentId].childFields!,
            -      ];
            -    }
               }
             
               return {
            @@ -202,15 +196,19 @@ export const reducer = (state: State, action: Action): State => {
                   let { rootLevelFields } = state.fields;
             
                   if (parentId) {
            -        // Deleting a child field
            -        const parentField = state.fields.byId[parentId];
            +        // Deleting a child field, we need to update its parent meta
            +        const parentField = { ...state.fields.byId[parentId] };
                     parentField.childFields = parentField.childFields!.filter(childId => childId !== id);
            -        parentField.hasChildFields = Boolean(parentField.childFields.length);
            -        parentField.hasMultiFields = Boolean(parentField.childFields.length);
            +        parentField.hasChildFields =
            +          parentField.canHaveChildFields && Boolean(parentField.childFields.length);
            +        parentField.hasMultiFields =
            +          parentField.canHaveMultiFields && Boolean(parentField.childFields.length);
             
            -        if (!parentField.hasChildFields) {
            +        if (!parentField.hasChildFields && !parentField.hasMultiFields) {
                       parentField.isExpanded = false;
                     }
            +
            +        state.fields.byId[parentId] = parentField;
                   } else {
                     // Deleting a root level field
                     rootLevelFields = rootLevelFields.filter(childId => childId !== id);
            @@ -222,6 +220,8 @@ export const reducer = (state: State, action: Action): State => {
                       delete state.fields.byId[childField.id];
                     });
                   }
            +
            +      // Finally... we delete the field from our map
                   delete state.fields.byId[id];
             
                   const maxNestedDepth = getMaxNestedDepth(state.fields.byId);
            @@ -286,17 +286,6 @@ export const reducer = (state: State, action: Action): State => {
             
                   state.fields.byId[fieldToEdit] = newField;
             
            -      // We _also_ need to make a copy of the parent "childFields"
            -      // array to force a re-render in the view.
            -      const { parentId } = newField;
            -      const rootLevelFields = parentId
            -        ? state.fields.rootLevelFields
            -        : [...state.fields.rootLevelFields]; // No parent, we need to make a copy of the "rootLevelFields"
            -
            -      if (parentId) {
            -        state.fields.byId[parentId].childFields = [...state.fields.byId[parentId].childFields!];
            -      }
            -
                   return {
                     ...state,
                     isValid: isStateValid(state),
            @@ -306,30 +295,16 @@ export const reducer = (state: State, action: Action): State => {
                       fieldToEdit: undefined,
                       status: 'idle',
                     },
            -        fields: {
            -          ...state.fields,
            -          rootLevelFields,
            -        },
                   };
                 }
                 case 'field.toggleExpand': {
            -      const updatedField: NormalizedField = {
            -        ...state.fields.byId[action.value.fieldId],
            -        isExpanded:
            -          action.value.isExpanded === undefined
            -            ? !state.fields.byId[action.value.fieldId].isExpanded
            -            : action.value.isExpanded,
            -      };
            -
            -      const rootLevelFields = updatedField.parentId
            -        ? state.fields.rootLevelFields
            -        : [...state.fields.rootLevelFields];
            +      const { fieldId, isExpanded } = action.value;
            +      const previousField = state.fields.byId[fieldId];
             
            -      if (updatedField.parentId) {
            -        state.fields.byId[updatedField.parentId].childFields = [
            -          ...state.fields.byId[updatedField.parentId].childFields!,
            -        ];
            -      }
            +      const nextField: NormalizedField = {
            +        ...previousField,
            +        isExpanded: isExpanded === undefined ? !previousField.isExpanded : isExpanded,
            +      };
             
                   return {
                     ...state,
            @@ -337,9 +312,8 @@ export const reducer = (state: State, action: Action): State => {
                       ...state.fields,
                       byId: {
                         ...state.fields.byId,
            -            [action.value.fieldId]: updatedField,
            +            [fieldId]: nextField,
                       },
            -          rootLevelFields,
                     },
                   };
                 }
            
            From ae69039c50ef8ac9beb3bf1303a2ce394ea63d35 Mon Sep 17 00:00:00 2001
            From: =?UTF-8?q?Se=CC=81bastien?= 
            Date: Wed, 23 Oct 2019 17:00:57 +0200
            Subject: [PATCH 41/42] Fix paddingLeft styling
            
            ---
             .../public/components/mappings_editor/_index.scss    | 12 ++++++++----
             1 file changed, 8 insertions(+), 4 deletions(-)
            
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss
            index 10bb08b484706d..ed6efbcedfe757 100644
            --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss
            +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/_index.scss
            @@ -121,8 +121,16 @@
                   }
                 }
             
            +    .mappings-editor__create-field-content {
            +      padding-left: $euiSizeXXL - $euiSizeXS; // [1]
            +    }
            +
                 .mappings-editor__create-field-wrapper {
                   &--multi-field {
            +        .mappings-editor__create-field-content {
            +          padding-left: $euiSize;
            +        }
            +
                     .mappings-editor__create-field-content {
                       &::before, &::after {
                         content: none;
            @@ -137,10 +145,6 @@
                   }
                 }
             
            -    .mappings-editor__create-field-content {
            -      padding-left: $euiSize
            -    }
            -
                 .mappings-editor__fields-list .mappings-editor__fields-list-item__content {
                   padding-left: $euiSizeXL; // [2]
             
            
            From e03962d99c9df5bca7a96fb0cc4074125f095eec Mon Sep 17 00:00:00 2001
            From: =?UTF-8?q?Se=CC=81bastien?= 
            Date: Wed, 23 Oct 2019 17:27:11 +0200
            Subject: [PATCH 42/42] Fix display modal to confirm type change
            
            ---
             .../edit_field/update_field_provider.tsx      |  4 ++--
             .../fields/fields_list_item_container.tsx     | 22 +++++++++----------
             2 files changed, 13 insertions(+), 13 deletions(-)
            
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
            index 007a043e3eec79..dda17bee83dd2a 100644
            --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
            +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx
            @@ -44,9 +44,9 @@ export const UpdateFieldProvider = ({ children }: Props) => {
                   oldType: DataType,
                   newType: DataType
                 ): { requiresConfirmation: boolean } => {
            -      const { hasChildFields, hasMultiFields, canHaveChildFields, canHaveMultiFields } = field;
            +      const { hasChildFields, hasMultiFields } = field;
             
            -      if ((!hasChildFields && canHaveChildFields) || (!hasMultiFields && canHaveMultiFields)) {
            +      if (!hasChildFields && !hasMultiFields) {
                     // No child or multi-fields will be deleted, no confirmation needed.
                     return { requiresConfirmation: false };
                   }
            diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
            index a1ac25d7f80996..0ea07dc4173907 100644
            --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
            +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx
            @@ -25,33 +25,33 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop
               const getField = (id: string) => byId[id];
             
               const field: NormalizedField = getField(fieldId);
            -  const { id, childFields, hasChildFields, hasMultiFields } = field;
            -  const isHighlighted = fieldToEdit === id;
            -  const isDimmed = status === 'editingField' && fieldToEdit !== id;
            -  const isCreateFieldFormVisible = status === 'creatingField' && fieldToAddFieldTo === id;
            +  const { childFields } = field;
            +  const isHighlighted = fieldToEdit === fieldId;
            +  const isDimmed = status === 'editingField' && fieldToEdit !== fieldId;
            +  const isCreateFieldFormVisible = status === 'creatingField' && fieldToAddFieldTo === fieldId;
               const areActionButtonsVisible = status === 'idle';
               const childFieldsArray = useMemo(
            -    () => (hasChildFields || hasMultiFields ? childFields!.map(getField) : []),
            +    () => (childFields !== undefined ? childFields.map(getField) : []),
                 [childFields]
               );
             
               const addField = useCallback(() => {
                 dispatch({
                   type: 'documentField.createField',
            -      value: id,
            +      value: fieldId,
                 });
            -  }, [id]);
            +  }, [fieldId]);
             
               const editField = useCallback(() => {
                 dispatch({
                   type: 'documentField.editField',
            -      value: id,
            +      value: fieldId,
                 });
            -  }, [id]);
            +  }, [fieldId]);
             
               const toggleExpand = useCallback(() => {
            -    dispatch({ type: 'field.toggleExpand', value: { fieldId: id } });
            -  }, [id]);
            +    dispatch({ type: 'field.toggleExpand', value: { fieldId } });
            +  }, [fieldId]);
             
               return (