diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 3f4050e98f64d..69c926e426c45 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { FieldHook, FIELD_TYPES } from '../hook_form_lib'; interface Props { field: FieldHook; - euiFieldProps?: Record; + euiFieldProps?: { [key: string]: any }; idAria?: string; [key: string]: any; } @@ -34,16 +34,18 @@ import { ComboBoxField, MultiSelectField, SelectField, + SuperSelectField, ToggleField, } from './fields'; -const mapTypeToFieldComponent = { +const mapTypeToFieldComponent: { [key: string]: ComponentType } = { [FIELD_TYPES.TEXT]: TextField, [FIELD_TYPES.NUMBER]: NumericField, [FIELD_TYPES.CHECKBOX]: CheckBoxField, [FIELD_TYPES.COMBO_BOX]: ComboBoxField, [FIELD_TYPES.MULTI_SELECT]: MultiSelectField, [FIELD_TYPES.SELECT]: SelectField, + [FIELD_TYPES.SUPER_SELECT]: SuperSelectField, [FIELD_TYPES.TOGGLE]: ToggleField, }; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/index.ts b/src/plugins/es_ui_shared/static/forms/components/fields/index.ts index 4e9d3c5624226..b8ee992ef1831 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/index.ts +++ b/src/plugins/es_ui_shared/static/forms/components/fields/index.ts @@ -23,5 +23,6 @@ export * from './checkbox_field'; export * from './combobox_field'; export * from './multi_select_field'; export * from './select_field'; +export * from './super_select_field'; export * from './toggle_field'; export * from './text_area_field'; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx index b7eb1c5fa3bd3..ca889bda5bc08 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { ReactNode, OptionHTMLAttributes } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { FieldHook } from '../../hook_form_lib'; @@ -25,12 +25,17 @@ import { getFieldValidityAndErrorMessage } from '../helpers'; interface Props { field: FieldHook; - euiFieldProps?: Record; + euiFieldProps: { + options: Array< + { text: string | ReactNode; [key: string]: any } & OptionHTMLAttributes + >; + [key: string]: any; + }; idAria?: string; [key: string]: any; } -export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { +export const SelectField = ({ field, euiFieldProps, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( @@ -52,7 +57,7 @@ export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { hasNoInitialSelection={true} isInvalid={isInvalid} data-test-subj="select" - {...(euiFieldProps as { options: any; [key: string]: any })} + {...euiFieldProps} /> ); diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx new file mode 100644 index 0000000000000..5ac9913ccc1d5 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFormRow, EuiSuperSelect, EuiSuperSelectProps } from '@elastic/eui'; + +import { FieldHook } from '../../hook_form_lib'; +import { getFieldValidityAndErrorMessage } from '../helpers'; + +interface Props { + field: FieldHook; + euiFieldProps: { + options: EuiSuperSelectProps['options']; + [key: string]: any; + }; + idAria?: string; + [key: string]: any; +} + +export const SuperSelectField = ({ field, euiFieldProps, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + { + field.setValue(value); + }} + isInvalid={isInvalid} + data-test-subj="superSelect" + options + {...euiFieldProps} + /> + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts index 92dd7286e84ac..888e29ed38693 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts @@ -25,3 +25,4 @@ export * from './index_name'; export * from './contains_char'; export * from './starts_with'; export * from './index_pattern_field'; +export * from './lowercase_string'; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts new file mode 100644 index 0000000000000..42de66b930eaa --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc } from '../../hook_form_lib'; +import { isLowerCaseString } from '../../../validators/string'; +import { ERROR_CODE } from './types'; + +export const lowerCaseStringField = (message: string) => ( + ...args: Parameters +): ReturnType> => { + const [{ value }] = args; + + if (typeof value !== 'string') { + return; + } + + if (!isLowerCaseString(value)) { + return { + code: 'ERR_LOWERCASE_STRING', + message, + }; + } +}; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts index 25cf038ec227d..f87314bf4f963 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts @@ -25,4 +25,5 @@ export type ERROR_CODE = | 'ERR_MIN_LENGTH' | 'ERR_MAX_LENGTH' | 'ERR_MIN_SELECTION' - | 'ERR_MAX_SELECTION'; + | 'ERR_MAX_SELECTION' + | 'ERR_LOWERCASE_STRING'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 3226b7735703f..9c3a563daf038 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -51,7 +51,7 @@ export const FormDataProvider = ({ children, pathsToWatch }: Props) => { }); return subscription.unsubscribe; - }, [pathsToWatch]); + }, [form, pathsToWatch]); return children(formData); }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index 2df06b206976a..7d340c5a8e3b9 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -29,6 +29,7 @@ export interface Props { defaultValue?: unknown; component?: FunctionComponent | 'input'; componentProps?: Record; + onChange?: (value: unknown) => void; children?: (field: FieldHook) => JSX.Element; } @@ -38,6 +39,7 @@ export const UseField = ({ defaultValue, component = 'input', componentProps = {}, + onChange, children, }: Props) => { const form = useFormContext(); @@ -64,7 +66,7 @@ export const UseField = ({ } } - const field = useField(form, path, configCopy); + const field = useField(form, path, configCopy, onChange); // Remove field from form when it is unmounted or if its path changes useEffect(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts index f65b7cd0aa0b0..9ca7ae59e7a4a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts @@ -26,6 +26,7 @@ export const FIELD_TYPES = { COMBO_BOX: 'comboBox', SELECT: 'select', MULTI_SELECT: 'multiSelect', + SUPER_SELECT: 'superSelect', }; // Validation types diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx index b7c6c39e7b0c5..5dcd076b41533 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx @@ -19,7 +19,7 @@ import React, { createContext, useContext } from 'react'; -import { FormHook } from './types'; +import { FormHook, FormData } from './types'; const FormContext = createContext | undefined>(undefined); @@ -32,7 +32,7 @@ export const FormProvider = ({ children, form }: Props) => ( {children} ); -export const useFormContext = function>() { +export const useFormContext = function() { const context = useContext(FormContext) as FormHook; if (context === undefined) { throw new Error('useFormContext must be used within a '); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index b2e9ccab53017..ec04ead0d6876 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -22,7 +22,12 @@ import { useState, useEffect, useRef } from 'react'; import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; -export const useField = (form: FormHook, path: string, config: FieldConfig = {}) => { +export const useField = ( + form: FormHook, + path: string, + config: FieldConfig = {}, + valueChangeListener?: (value: unknown) => void +) => { const { type = FIELD_TYPES.TEXT, defaultValue = '', @@ -49,6 +54,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) const changeCounter = useRef(0); const inflightValidation = useRef | null>(null); const debounceTimeout = useRef(null); + const isUnmounted = useRef(false); // -- HELPERS // ---------------------------------- @@ -96,12 +102,23 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) setIsChangingValue(true); } + const newValue = serializeOutput(value); + + // Notify listener + if (valueChangeListener) { + valueChangeListener(newValue); + } + // Update the form data observable - form.__updateFormDataAt(path, serializeOutput(value)); + form.__updateFormDataAt(path, newValue); // Validate field(s) and set form.isValid flag await form.__validateFields(fieldsToValidateOnChange); + if (isUnmounted.current) { + return; + } + /** * If we have set a delay to display the error message after the field value has changed, * we first check that this is the last "change iteration" (=== the last keystroke from the user) @@ -398,6 +415,15 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) }; }, [value]); + /** + * On unmount + */ + useEffect(() => { + return () => { + isUnmounted.current = true; + }; + }, []); + const field: FieldHook = { path, type, 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 1cca9ba493303..8a22959f38289 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 @@ -29,11 +29,11 @@ const DEFAULT_OPTIONS = { stripEmptyFields: true, }; -interface UseFormReturn { +interface UseFormReturn { form: FormHook; } -export function useForm( +export function useForm( formConfig: FormConfig | undefined = {} ): UseFormReturn { const { @@ -159,10 +159,11 @@ export function useForm( const addField: FormHook['__addField'] = field => { fieldsRefs.current[field.path] = field; - // Only update the formData if the path does not exist (it is the _first_ time - // the field is added), to avoid entering an infinite loop when the form is re-rendered. - if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) { - updateFormDataAt(field.path, field.__serializeOutput()); + const currentValue = formData$.current.value[field.path]; + const fieldValue = field.__serializeOutput(); + + if (currentValue !== fieldValue) { + updateFormDataAt(field.path, fieldValue); } }; @@ -179,10 +180,16 @@ export function useForm( }; const setFieldValue: FormHook['setFieldValue'] = (fieldName, value) => { + if (fieldsRefs.current[fieldName] === undefined) { + return; + } fieldsRefs.current[fieldName].setValue(value); }; const setFieldErrors: FormHook['setFieldErrors'] = (fieldName, errors) => { + if (fieldsRefs.current[fieldName] === undefined) { + return; + } fieldsRefs.current[fieldName].setErrors(errors); }; @@ -221,11 +228,10 @@ export function useForm( const subscribe: FormHook['subscribe'] = handler => { const format = () => serializer(getFormData() as T); - const validate = async () => await validateAllFields(); const subscription = formData$.current.subscribe(raw => { if (!isUnmounted.current) { - handler({ isValid, data: { raw, format }, validate }); + handler({ isValid, data: { raw, format }, validate: validateAllFields }); } }); @@ -241,8 +247,13 @@ export function useForm( const { resetValues = true } = resetOptions; const currentFormData = { ...formData$.current.value } as FormData; Object.entries(fieldsRefs.current).forEach(([path, field]) => { - const fieldValue = field.reset({ resetValue: resetValues }); - currentFormData[path] = fieldValue; + // By resetting the form, some field might be unmounted. In order + // to avoid a race condition, we check that the field still exists. + const isFieldMounted = fieldsRefs.current[path] !== undefined; + if (isFieldMounted) { + const fieldValue = field.reset({ resetValue: resetValues }); + currentFormData[path] = fieldValue; + } }); if (resetValues) { formData$.current.next(currentFormData as T); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index b19e4be9e3e6c..39a48c77bdf64 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -22,9 +22,9 @@ import { Subject, Subscription } from './lib'; // This type will convert all optional property to required ones // Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623 -type Required = T extends object ? { [P in keyof T]-?: NonNullable } : T; +type Required = T extends FormData ? { [P in keyof T]-?: NonNullable } : T; -export interface FormHook { +export interface FormHook { readonly isSubmitted: boolean; readonly isSubmitting: boolean; readonly isValid: boolean | undefined; @@ -47,16 +47,16 @@ export interface FormHook { __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; } -export interface FormSchema { +export interface FormSchema { [key: string]: FormSchemaEntry; } -type FormSchemaEntry = +type FormSchemaEntry = | FieldConfig | Array> | { [key: string]: FieldConfig | Array> | FormSchemaEntry }; -export interface FormConfig { +export interface FormConfig { onSubmit?: (data: T, isFormValid: boolean) => void; schema?: FormSchema; defaultValue?: Partial; @@ -65,7 +65,7 @@ export interface FormConfig { options?: FormOptions; } -export interface OnFormUpdateArg { +export interface OnFormUpdateArg { data: { raw: { [key: string]: any }; format: () => T; @@ -74,7 +74,7 @@ export interface OnFormUpdateArg { isValid?: boolean; } -export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; +export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; export interface FormOptions { errorDisplayDelay?: number; @@ -113,7 +113,7 @@ export interface FieldHook { __serializeOutput: (rawValue?: unknown) => unknown; } -export interface FieldConfig { +export interface FieldConfig { readonly path?: string; readonly label?: string; readonly helpText?: string | ReactNode; @@ -140,7 +140,7 @@ export interface ValidationError { [key: string]: any; } -export interface ValidationFuncArg { +export interface ValidationFuncArg { path: string; value: V; form: FormHook; @@ -148,7 +148,7 @@ export interface ValidationFuncArg { errors: readonly ValidationError[]; } -export type ValidationFunc = ( +export type ValidationFunc = ( data: ValidationFuncArg ) => ValidationError | void | undefined | Promise | void | undefined>; @@ -169,7 +169,7 @@ type FormatterFunc = (value: any, formData: FormData) => unknown; // string | number | boolean | string[] ... type FieldValue = unknown; -export interface ValidationConfig { +export interface ValidationConfig { validator: ValidationFunc; type?: string; exitOnFail?: boolean; diff --git a/src/plugins/es_ui_shared/static/validators/string/index.ts b/src/plugins/es_ui_shared/static/validators/string/index.ts index 0ddf6fdfc33e4..1e80ca0700637 100644 --- a/src/plugins/es_ui_shared/static/validators/string/index.ts +++ b/src/plugins/es_ui_shared/static/validators/string/index.ts @@ -25,3 +25,4 @@ export * from './is_empty'; export * from './is_url'; export * from './starts_with'; export * from './is_json'; +export * from './is_lowercase'; diff --git a/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts b/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts new file mode 100644 index 0000000000000..3d765a750a81a --- /dev/null +++ b/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isLowerCaseString = (value: string) => value.toLowerCase() === value; 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 20233c7543112..b30588a6f0dc8 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 @@ -10,22 +10,22 @@ border-bottom is always visible, even when mouseovering and changing the background color. */ -.mappings-editor { - &__fields-list-item { - &--dotted-line { - > .mappings-editor__fields-list-item__field { +.mappingsEditor { + &__fieldsListItem { + &--dottedLine { + > .mappingsEditor__fieldsListItem__field { border-bottom-style: dashed; } } &__field { border-bottom: $euiBorderThin; - margin-top: 4px; // [3] + margin-top: $euiSizeXS; // [3] &:hover { background-color: $euiColorLightestShade; - .mappings-editor__fields-list-item__actions, - .mappings-editor__fields-list-item__multi-field-button { + .mappingsEditor__fieldsListItem__actions, + .mappingsEditor__fieldsListItem__multiFieldButton { opacity: 1; } } @@ -69,14 +69,14 @@ } &__actions, - &__multi-field-button { + &__multiFieldButton { opacity: 0; padding-right: $euiSizeS; transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; } } - &__create-field-wrapper { + &__createFieldWrapper { background-color: $euiColorLightestShade; border-right: $euiBorderThin; border-bottom: $euiBorderThin; @@ -84,17 +84,17 @@ padding: $euiSize; } - &__create-field-content { + &__createFieldContent { position: relative; } - &__create-field-required-props { + &__createFieldRequiredProps { margin-top: $euiSizeL; padding-top: $euiSize; border-top: 1px solid $euiColorLightShade; } - &__edit-field { + &__editField { min-width: 680px; &__section { @@ -106,7 +106,7 @@ margin-bottom: 0; } - + .mappings-editor__edit-field__advanced-settings { + + .mappingsEditor__editField__advancedSettings { margin-top: $euiSizeL * -1; } } @@ -128,25 +128,25 @@ &__title { line-height: 1; - margin-bottom: 8px; + margin-bottom: $euiSizeS; } } } - &__select-with-custom { + &__selectWithCustom { position: relative; &__button { position: absolute; right: 0; - top: $euiSizeXS * -1; + top: 0; } } } -.mappings-editor__fields-list { - .mappings-editor__fields-list .mappings-editor__fields-list-item__content, - .mappings-editor__create-field-content { +.mappingsEditor__fieldsList { + .mappingsEditor__fieldsList .mappingsEditor__fieldsListItem__content, + .mappingsEditor__createFieldContent { &::before { border-bottom: 1px solid $euiColorMediumShade; content: ''; @@ -165,17 +165,17 @@ } } - .mappings-editor__create-field-content { + .mappingsEditor__createFieldContent { padding-left: $euiSizeXXL - $euiSizeXS; // [1] } - .mappings-editor__create-field-wrapper { - &--multi-field { - .mappings-editor__create-field-content { + .mappingsEditor__createFieldWrapper { + &--multiField { + .mappingsEditor__createFieldContent { padding-left: $euiSize; } - .mappings-editor__create-field-content { + .mappingsEditor__createFieldContent { &::before, &::after { content: none; } @@ -183,16 +183,16 @@ } &--toggle { - .mappings-editor__create-field-content { + .mappingsEditor__createFieldContent { padding-left: $euiSizeXXL - $euiSizeXS; // [1] } } } - .mappings-editor__fields-list .mappings-editor__fields-list-item__content { + .mappingsEditor__fieldsList .mappingsEditor__fieldsListItem__content { padding-left: $euiSizeXL; // [2] - &--toggle, &--multi-field { + &--toggle, &--multiField { &::before, &::after { content: none; } @@ -202,26 +202,26 @@ padding-left: 0; } - &--multi-field { + &--multiField { padding-left: $euiSizeS; } } } -ul.tree { +ul.esUiTree { padding: 0; margin: 0; list-style-type: none; position: relative; padding-top: $euiSizeXS; - li.tree-item { + li.esUiTreeItem { list-style-type: none; border-left: $euiBorderThin; margin-left: $euiSizeL; padding-bottom: $euiSizeS; - div { + &__label { padding-left: $euiSizeL; position: relative; @@ -239,10 +239,11 @@ ul.tree { } } - > li.tree-item:first-child { + > li.esUiTreeItem:first-child { padding-top: $euiSizeS; } - > li.tree-item:last-child { + + > li.esUiTreeItem:last-child { border-left-color: transparent; padding-bottom: 0; } 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 8ef6587e8096f..d34d1e964e889 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 @@ -35,7 +35,7 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { }, [form]); return ( -
+ = { dynamic: { - label: i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicFieldLabel', { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.dynamicFieldLabel', { defaultMessage: 'Dynamic field', }), - helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicFieldDescription', { + helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.dynamicFieldDescription', { defaultMessage: 'Allow new fields discovery in document.', }), type: FIELD_TYPES.SELECT, defaultValue: true, }, date_detection: { - label: i18n.translate('xpack.idxMgmt.mappingsEditor.dateDetectionFieldLabel', { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.dateDetectionFieldLabel', { defaultMessage: 'Date detection', }), - helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.dateDetectionFieldDescription', { - defaultMessage: 'Check if the string field is a date.', - }), + helpText: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.dateDetectionFieldDescription', + { + defaultMessage: 'Check if the string field is a date.', + } + ), type: FIELD_TYPES.TOGGLE, defaultValue: true, }, numeric_detection: { - label: i18n.translate('xpack.idxMgmt.mappingsEditor.numericFieldLabel', { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.numericFieldLabel', { defaultMessage: 'Numeric field', }), - helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.numericFieldDescription', { + helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.numericFieldDescription', { defaultMessage: 'Check if the string field is a numeric value.', }), type: FIELD_TYPES.TOGGLE, defaultValue: true, }, dynamic_date_formats: { - label: i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicDatesFieldLabel', { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.dynamicDatesFieldLabel', { defaultMessage: 'Dynamic dates format', }), - helpText: i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicDatesFieldDescription', { - defaultMessage: - 'The dynamic_date_formats can be customised to support your own date formats.', - }), + helpText: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.dynamicDatesFieldDescription', + { + defaultMessage: + 'The dynamic_date_formats can be customised to support your own date formats.', + } + ), type: FIELD_TYPES.COMBO_BOX, defaultValue: [], validations: [ { validator: containsCharsField({ message: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.dynamicDatesFieldValidationErrorMessage', + 'xpack.idxMgmt.mappingsEditor.configuration.dynamicDatesFieldValidationErrorMessage', { defaultMessage: 'Spaces are not allowed.', } 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 d6a254135dff0..1df063f6ffe35 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 @@ -9,7 +9,7 @@ import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useMappingsState, useDispatch } from '../../mappings_state'; -import { FieldsList, CreateField, EditField } from './fields'; +import { FieldsList, CreateField, EditFieldContainer } from './fields'; export const DocumentFields = () => { const dispatch = useDispatch(); @@ -61,7 +61,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/analyzer_parameter.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx new file mode 100644 index 0000000000000..94e1e12c9297a --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { UseField, TextField, FieldConfig, FieldHook } from '../../../shared_imports'; +import { getFieldConfig } from '../../../lib'; +import { PARAMETERS_OPTIONS, getSuperSelectOption } from '../../../constants'; +import { + IndexSettings, + IndexSettingsInterface, + SelectOption, + SuperSelectOption, +} from '../../../types'; +import { useIndexSettings } from '../../../index_settings_context'; +import { AnalyzerParameterSelects } from './analyzer_parameter_selects'; + +interface Props { + path: string; + defaultValue: string | undefined; + label?: string; + config?: FieldConfig; +} + +const ANALYZER_OPTIONS = PARAMETERS_OPTIONS.analyzer!; + +const getCustomAnalyzers = (indexSettings: IndexSettings): SelectOption[] | undefined => { + const settings: IndexSettingsInterface = {}.hasOwnProperty.call(indexSettings, 'index') + ? (indexSettings as { index: IndexSettingsInterface }).index + : (indexSettings as IndexSettingsInterface); + + if ( + !{}.hasOwnProperty.call(settings, 'analysis') || + !{}.hasOwnProperty.call(settings.analysis!, 'analyzer') + ) { + return undefined; + } + + // We wrap inside a try catch as the index settings are written in JSON + // and who knows what the user has entered. + try { + return Object.keys(settings.analysis!.analyzer).map(value => ({ value, text: value })); + } catch { + return undefined; + } +}; + +export interface MapOptionsToSubOptions { + [key: string]: { + label: string; + options: SuperSelectOption[] | SelectOption[]; + }; +} + +export const AnalyzerParameter = ({ path, defaultValue, label, config }: Props) => { + const indexSettings = useIndexSettings(); + const customAnalyzers = getCustomAnalyzers(indexSettings); + + const fieldOptions = [...ANALYZER_OPTIONS] as SuperSelectOption[]; + const mapOptionsToSubOptions: MapOptionsToSubOptions = { + language: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.analyzers.languageAnalyzerLabel', { + defaultMessage: 'Language', + }), + options: PARAMETERS_OPTIONS.languageAnalyzer!, + }, + }; + + if (customAnalyzers) { + const customOption: SuperSelectOption = { + value: 'custom', + ...getSuperSelectOption( + i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.customTitle', { + defaultMessage: 'Custom analyzer', + }), + i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.customDescription', { + defaultMessage: 'Choose one of your custom analyzers.', + }) + ), + }; + fieldOptions.push(customOption); + + mapOptionsToSubOptions.custom = { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.analyzers.customAnalyzerLabel', { + defaultMessage: 'Custom', + }), + options: customAnalyzers, + }; + } + + const isDefaultValueInOptions = + defaultValue === undefined || fieldOptions.some((option: any) => option.value === defaultValue); + + let mainValue: string | undefined = defaultValue; + let subValue: string | undefined; + let isDefaultValueInSubOptions = false; + + if (!isDefaultValueInOptions && mapOptionsToSubOptions !== undefined) { + // Check if the default value is one of the subOptions + for (const [key, subOptions] of Object.entries(mapOptionsToSubOptions)) { + if (subOptions.options.some((option: any) => option.value === defaultValue)) { + isDefaultValueInSubOptions = true; + mainValue = key; + subValue = defaultValue; + break; + } + } + } + + const [isCustom, setIsCustom] = useState( + !isDefaultValueInOptions && !isDefaultValueInSubOptions + ); + + const fieldConfig = config ? config : getFieldConfig('analyzer'); + const fieldConfigWithLabel = label !== undefined ? { ...fieldConfig, label } : fieldConfig; + + const toggleCustom = (field: FieldHook) => () => { + if (isCustom) { + field.setValue(fieldOptions[0].value); + } else { + field.setValue(''); + } + + field.reset({ resetValue: false }); + + setIsCustom(!isCustom); + }; + + return ( + + {field => ( +
+ + {isCustom + ? i18n.translate('xpack.idxMgmt.mappingsEditor.predefinedButtonLabel', { + defaultMessage: 'Use predefined', + }) + : i18n.translate('xpack.idxMgmt.mappingsEditor.customButtonLabel', { + defaultMessage: 'Add custom', + })} + + + {isCustom ? ( + // Wrap inside a flex item to maintain the same padding + // around the field. + + + + + + ) : ( + + )} +
+ )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx new file mode 100644 index 0000000000000..de3d70db31af4 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -0,0 +1,115 @@ +/* + * 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, useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + useForm, + Form, + UseField, + SelectField, + SuperSelectField, + FieldConfig, + FieldHook, + FormDataProvider, +} from '../../../shared_imports'; +import { SelectOption, SuperSelectOption } from '../../../types'; +import { MapOptionsToSubOptions } from './analyzer_parameter'; + +type Options = SuperSelectOption[] | SelectOption[]; + +const areOptionsSuperSelect = (options: Options): boolean => { + if (!options || !Boolean(options.length)) { + return false; + } + // `Select` options have a "text" property, `SuperSelect` options don't have it. + return {}.hasOwnProperty.call(options[0], 'text') === false; +}; + +interface Props { + onChange(value: unknown): void; + mainDefaultValue: string | undefined; + subDefaultValue: string | undefined; + config: FieldConfig; + options: Options; + mapOptionsToSubOptions: MapOptionsToSubOptions; +} + +export const AnalyzerParameterSelects = ({ + onChange, + mainDefaultValue, + subDefaultValue, + config, + options, + mapOptionsToSubOptions, +}: Props) => { + const { form } = useForm({ defaultValue: { main: mainDefaultValue, sub: subDefaultValue } }); + + useEffect(() => { + const subscription = form.subscribe(updateData => { + const formData = updateData.data.raw; + const value = formData.sub ? formData.sub : formData.main; + onChange(value); + }); + + return subscription.unsubscribe; + }, [form]); + + const getSubOptionsMeta = (mainValue: string) => + mapOptionsToSubOptions !== undefined ? mapOptionsToSubOptions[mainValue] : undefined; + + const onMainValueChange = useCallback((mainValue: unknown) => { + const subOptionsMeta = getSubOptionsMeta(mainValue as string); + form.setFieldValue('sub', subOptionsMeta ? subOptionsMeta.options[0].value : undefined); + }, []); + + const renderSelect = (field: FieldHook, opts: Options) => { + const isSuperSelect = areOptionsSuperSelect(opts); + + return isSuperSelect ? ( + + ) : ( + + ); + }; + + return ( + + + {({ main }) => { + const subOptions = getSubOptionsMeta(main); + + return ( + + + + {field => renderSelect(field, options)} + + + {subOptions && ( + + + {field => renderSelect(field, subOptions.options)} + + + )} + + ); + }} + + + ); +}; 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 index 1de2fd18363f5..69b3f38cd1027 100644 --- 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 @@ -14,6 +14,8 @@ export * from './doc_values_parameter'; export * from './boost_parameter'; +export * from './analyzer_parameter'; + export * from './null_value_parameter'; export * from './eager_global_ordinals_parameter'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx index cf83a2cdec017..a46c85b55766e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx @@ -5,20 +5,17 @@ */ import React from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; import { EditFieldFormRow } from '../fields/edit_field'; import { PARAMETERS_OPTIONS } from '../../../constants'; import { getFieldConfig } from '../../../lib'; -import { SelectOption } from '../../../types'; +import { SuperSelectOption } from '../../../types'; import { UseField, Field } from '../../../shared_imports'; interface Props { hasIndexOptions?: boolean; - indexOptions?: SelectOption[]; + indexOptions?: SuperSelectOption[]; } export const IndexParameter = ({ @@ -41,28 +38,16 @@ export const IndexParameter = ({ > {/* index_options */} {hasIndexOptions && ( - - - - - - - {i18n.translate('xpack.idxMgmt.mappingsEditor.indexOptionsFieldDescription', { - defaultMessage: 'Information that should be stored in the index.', - })} - - - + )} ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx index 23cbc8937668f..8880e988e2447 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiFormRow, EuiComboBox } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormRow, UseField, SerializerFunc } from '../../../shared_imports'; @@ -58,53 +58,76 @@ interface Props { field?: NormalizedField; } -export const PathParameter = ({ field, allFields }: Props) => ( - - {pathField => { - const error = pathField.getErrorsMessages(); - const isInvalid = error ? Boolean(error.length) : false; +export const PathParameter = ({ field, allFields }: Props) => { + const suggestedFields = getSuggestedFields(allFields, field); - return ( - - {i18n.translate('xpack.idxMgmt.mappingsEditor.aliasType.aliasTargetFieldTitle', { - defaultMessage: 'Alias target', - })} - - } - description={i18n.translate( - 'xpack.idxMgmt.mappingsEditor.aliasType.aliasTargetFieldDescription', - { - defaultMessage: - 'Select the field you want your alias to point to. You will then be able to use the alias instead of the target field in search requests, and selected other APIs like field capabilities.', + return ( + + {pathField => { + const error = pathField.getErrorsMessages(); + const isInvalid = error ? Boolean(error.length) : false; + + return ( + + {i18n.translate('xpack.idxMgmt.mappingsEditor.aliasType.aliasTargetFieldTitle', { + defaultMessage: 'Alias target', + })} + } - )} - idAria="mappingsEditorPathParameter" - > - - pathField.setValue(value)} - isClearable={false} - /> - - - ); - }} - -); + <> + {!Boolean(suggestedFields.length) && ( + <> + +

+ {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.aliasType.noFieldsAddedWarningMessage', + { + defaultMessage: + 'You need to add at least one field before creating an alias.', + } + )} +

+
+ + + )} + + + pathField.setValue(value)} + isClearable={false} + /> + + +
+ ); + }} +
+ ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx index 2314501d180be..1642b6535b1ce 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx @@ -5,8 +5,6 @@ */ import React from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EditFieldFormRow } from '../fields/edit_field'; @@ -33,27 +31,15 @@ export const SimilarityParameter = ({ defaultToggleValue }: Props) => ( direction="column" toggleDefaultValue={defaultToggleValue} > - - - - - - - {i18n.translate('xpack.idxMgmt.mappingsEditor.setSimilarityFieldDefaultDescription', { - defaultMessage: 'Defaults to BM25.', - })} - - - + ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index 5e79232d2464c..a3a5661863cb2 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -23,7 +23,7 @@ import { TYPE_DEFINITION, FIELD_TYPES_OPTIONS, EUI_SIZE } from '../../../../cons import { useDispatch } from '../../../../mappings_state'; import { fieldSerializer, getFieldConfig, filterTypesForMultiField } from '../../../../lib'; -import { Field, MainType, NormalizedFields } from '../../../../types'; +import { Field, MainType, SubType, NormalizedFields, SelectOption } from '../../../../types'; import { NameParameter } from '../../field_parameters'; import { getParametersFormForType } from './required_parameters_forms'; @@ -37,6 +37,8 @@ interface Props { maxNestedDepth?: number; } +const typeFieldConfig = getFieldConfig('type'); + export const CreateField = React.memo(function CreateFieldComponent({ allFields, isMultiField, @@ -89,16 +91,46 @@ export const CreateField = React.memo(function CreateFieldComponent({ } }; - const renderFormFields = (type: MainType) => { + /** + * When we change the type, we need to check if there is a subType array to choose from. + * If there is a subType array, we build the options list for the select (and in case the field is a multi-field + * we also filter out blacklisted types). + * + * @param type The selected field type + */ + const getSubTypeMeta = ( + type: MainType + ): { subTypeLabel?: string; subTypeOptions?: Array> } => { const typeDefinition = TYPE_DEFINITION[type]; - const hasSubType = typeDefinition && typeDefinition.subTypes !== undefined; + const hasSubTypes = typeDefinition !== undefined && typeDefinition.subTypes; + + let subTypeOptions = hasSubTypes + ? typeDefinition + .subTypes!.types.map(subType => TYPE_DEFINITION[subType]) + .map( + subType => + ({ value: subType.value as SubType, text: subType.label } as SelectOption) + ) + : undefined; + + if (isMultiField && hasSubTypes) { + // If it is a multi-field, we need to filter out non-allowed types + subTypeOptions = filterTypesForMultiField(subTypeOptions!); + } + + return { + subTypeOptions, + subTypeLabel: hasSubTypes ? typeDefinition.subTypes!.label : undefined, + }; + }; + + const onTypeChange = (nextType: unknown) => { + const { subTypeOptions } = getSubTypeMeta(nextType as MainType); + form.setFieldValue('subType', subTypeOptions ? subTypeOptions[0].value : undefined); + }; - const subTypeOptions = - typeDefinition && typeDefinition.subTypes - ? typeDefinition.subTypes.types - .map(subType => TYPE_DEFINITION[subType]) - .map(subType => ({ value: subType.value, text: subType.label })) - : undefined; + const renderFormFields = (type: MainType) => { + const { subTypeOptions, subTypeLabel } = getSubTypeMeta(type); return ( @@ -111,7 +143,8 @@ export const CreateField = React.memo(function CreateFieldComponent({ {/* Field sub type (if any) */} - {hasSubType && ( + {subTypeOptions && (
0, - 'mappings-editor__create-field-wrapper--multi-field': isMultiField, + 'mappingsEditor__createFieldWrapper--multiField': isMultiField, })} style={{ paddingLeft: `${ @@ -187,10 +218,10 @@ export const CreateField = React.memo(function CreateFieldComponent({ }px`, }} > -
+
{isMultiField && ( - + )} @@ -206,7 +237,7 @@ export const CreateField = React.memo(function CreateFieldComponent({ {({ type, subType }) => { const ParametersForm = getParametersFormForType(type, subType); return ParametersForm ? ( -
+
) : null; 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 dd1bb1c2e53a4..ea716779098db 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 @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiBadge, EuiCode } from '@elastic/eui'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { useMappingsState, useDispatch } from '../../../mappings_state'; import { NormalizedField } from '../../../types'; -import { buildFieldTreeFromIds, getAllDescendantAliases } from '../../../lib'; -import { FieldsTree } from '../../fields_tree'; +import { getAllDescendantAliases } from '../../../lib'; +import { ModalConfirmationDeleteFields } from './modal_confirmation_delete_fields'; type DeleteFieldFunc = (property: NormalizedField) => void; @@ -31,9 +30,29 @@ export const DeleteFieldProvider = ({ children }: Props) => { const { fields } = useMappingsState(); const { byId } = fields; - const closeModal = () => { - setState({ isModalOpen: false }); - }; + const confirmButtonText = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.deleteField.confirmationModal.removeButtonLabel', + { + defaultMessage: 'Remove', + } + ); + + let modalTitle: string | undefined; + + if (state.field) { + const { isMultiField, source } = state.field; + + modalTitle = i18n.translate( + 'xpack.idxMgmt.mappingsEditor.deleteField.confirmationModal.title', + { + defaultMessage: "Remove {fieldType} '{fieldName}'?", + values: { + fieldType: isMultiField ? 'multi-field' : 'field', + fieldName: source.name, + }, + } + ); + } const deleteField: DeleteFieldFunc = field => { const { hasChildFields, hasMultiFields } = field; @@ -49,95 +68,30 @@ export const DeleteFieldProvider = ({ children }: Props) => { } }; + const closeModal = () => { + setState({ isModalOpen: false }); + }; + const confirmDelete = () => { dispatch({ type: 'field.remove', value: state.field!.id }); closeModal(); }; - const renderModal = () => { - const field = state.field!; - const { isMultiField, childFields } = field; - - const title = `Remove ${isMultiField ? 'multi-field' : 'field'} '${field.source.name}'?`; - - const fieldsTree = - childFields && childFields.length - ? buildFieldTreeFromIds(childFields, byId, (fieldItem: NormalizedField) => ( - <> - {fieldItem.source.name} - {fieldItem.isMultiField && ( - <> - {' '} - multi-field - - )} - - )) - : null; - - return ( - - - <> - {fieldsTree && ( - <> -

- {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription', - { - defaultMessage: 'This will also delete the following fields.', - } - )} -

- - - )} - {state.aliases && ( - <> -

- {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription', - { - defaultMessage: 'The following aliases will also be deleted.', - } - )} -

-
    - {state.aliases.map(path => ( -
  • - {path} -
  • - ))} -
- - )} - -
-
- ); - }; - return ( - + <> {children(deleteField)} - {state.isModalOpen && renderModal()} - + + {state.isModalOpen && ( + + )} + ); }; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/advanced_settings_wrapper.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/advanced_settings_wrapper.tsx index b8cfc17838147..91f9e8f6414e9 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/advanced_settings_wrapper.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/advanced_settings_wrapper.tsx @@ -19,19 +19,17 @@ export const AdvancedSettingsWrapper = ({ children }: Props) => { }; return ( -
+
{isVisible ? 'Hide' : 'Show'} advanced settings
- {isVisible && ( - <> - - {/* We ned to wrap the children inside a "div" to have our css :first-child rule */} -
{children}
- - )} +
+ + {/* We ned to wrap the children inside a "div" to have our css :first-child rule */} +
{children}
+
); }; 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 a283695c89dfa..61d0c41100ec9 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, { useEffect } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlyout, @@ -20,29 +20,39 @@ import { EuiCallOut, } from '@elastic/eui'; -import { useForm, Form, FormDataProvider } from '../../../../shared_imports'; -import { useDispatch } from '../../../../mappings_state'; +import { Form, FormHook, FormDataProvider } from '../../../../shared_imports'; import { TYPE_DEFINITION } from '../../../../constants'; -import { Field, NormalizedField, NormalizedFields, MainType, SubType } from '../../../../types'; -import { fieldSerializer, fieldDeserializer, getTypeDocLink } from '../../../../lib'; +import { + Field, + NormalizedField, + NormalizedFields, + DataType, + MainType, + SubType, +} from '../../../../types'; +import { getTypeDocLink } from '../../../../lib'; import { getParametersFormForType } from '../field_types'; import { UpdateFieldProvider, UpdateFieldFunc } from './update_field_provider'; import { EditFieldHeaderForm } from './edit_field_header_form'; import { EditFieldSection } from './edit_field_section'; +const limitStringLength = (text: string, limit = 18): string => { + if (text.length <= limit) { + return text; + } + + return `...${text.substr(limit * -1)}`; +}; + interface Props { + type: DataType; + form: FormHook; field: NormalizedField; allFields: NormalizedFields['byId']; + exitEdit(): void; } -export const EditField = React.memo(({ field, allFields }: Props) => { - const { form } = useForm({ - defaultValue: { ...field.source }, - serializer: fieldSerializer, - deserializer: fieldDeserializer, - }); - const dispatch = useDispatch(); - +export const EditField = React.memo(({ form, field, allFields, exitEdit }: Props) => { const getSubmitForm = (updateField: UpdateFieldFunc) => async (e?: React.FormEvent) => { if (e) { e.preventDefault(); @@ -55,18 +65,6 @@ export const EditField = React.memo(({ field, allFields }: Props) => { } }; - 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 cancel = () => { exitEdit(); }; @@ -95,43 +93,54 @@ export const EditField = React.memo(({ field, allFields }: Props) => { onClose={exitEdit} aria-labelledby="mappingsEditorFieldEditTitle" size="m" - className="mappings-editor__edit-field" + className="mappingsEditor__editField" maxWidth={720} > - -

- {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldTitle', { - defaultMessage: "Edit field '{fieldName}'", - values: { - fieldName: field.source.name, - }, - })} -

-
- {field.path} - -
- - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', - { - defaultMessage: '{type} documentation', - values: { - type: subTypeDefinition - ? subTypeDefinition.label - : typeDefinition.label, - }, - } - )} - -
+ + + {/* We need an extra div to get out of flex grow */} +
+ {/* Title */} + +

+ {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldTitle', { + defaultMessage: "Edit field '{fieldName}'", + values: { + fieldName: limitStringLength(field.source.name), + }, + })} +

+
+ + {/* Field path */} + {field.path} +
+
+ + {/* Documentation link */} + + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', + { + defaultMessage: '{type} documentation', + values: { + type: subTypeDefinition + ? subTypeDefinition.label + : typeDefinition.label, + }, + } + )} + + +
diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx new file mode 100644 index 0000000000000..c025a0713dcb8 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState, useEffect, useCallback } from 'react'; + +import { useForm } from '../../../../shared_imports'; +import { useDispatch } from '../../../../mappings_state'; +import { Field, NormalizedField, NormalizedFields, DataType } from '../../../../types'; +import { fieldSerializer, fieldDeserializer } from '../../../../lib'; +import { EditField } from './edit_field'; + +interface Props { + field: NormalizedField; + allFields: NormalizedFields['byId']; +} + +export const EditFieldContainer = React.memo(({ field, allFields }: Props) => { + const [type, setType] = useState(field.source.type); + const dispatch = useDispatch(); + + const { form } = useForm({ + defaultValue: { ...field.source }, + serializer: fieldSerializer, + deserializer: fieldDeserializer, + }); + + useEffect(() => { + const subscription = form.subscribe(updatedFieldForm => { + setType(updatedFieldForm.data.raw.subType || updatedFieldForm.data.raw.type); + dispatch({ type: 'fieldForm.update', value: updatedFieldForm }); + }); + + return subscription.unsubscribe; + }, [form]); + + const exitEdit = useCallback(() => { + dispatch({ type: 'documentField.changeStatus', value: 'idle' }); + }, []); + + return ( + + ); +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx index d652375b60837..83fd6aaa77cb6 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx @@ -8,7 +8,13 @@ import React, { useState, useRef } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiSwitch } from '@elastic/eui'; -import { ToggleField, UseField, FormDataProvider, FieldHook } from '../../../../shared_imports'; +import { + ToggleField, + UseField, + FormDataProvider, + FieldHook, + useFormContext, +} from '../../../../shared_imports'; import { ParameterName } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; @@ -41,6 +47,7 @@ export const EditFieldFormRow = React.memo( formFieldPath, children, }: Props) => { + const form = useFormContext(); const toggleField = useRef(undefined); const initialVisibleState = @@ -53,6 +60,13 @@ export const EditFieldFormRow = React.memo( const isChildrenFunction = typeof children === 'function'; const onToggle = () => { + if (isContentVisible === true) { + /** + * We are hiding the children (and thus removing any form field from the DOM). + * We need to reset the form to re-enable a possible disabled "save" button (from a previous validation error). + */ + form.reset({ resetValues: false }); + } setIsContentVisible(!isContentVisible); }; @@ -66,7 +80,8 @@ export const EditFieldFormRow = React.memo( const renderToggleInput = () => formFieldPath === undefined ? ( - + // TODO: Ask EUI why the "label" is a required prop since last update + ) : ( {field => { @@ -77,7 +92,7 @@ export const EditFieldFormRow = React.memo( ); const renderContent = () => ( - + {withToggle && ( {renderToggleInput()} @@ -95,12 +110,12 @@ export const EditFieldFormRow = React.memo(