diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts index 4110db5a39312..d5ad51ba35839 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/index.ts @@ -7,3 +7,5 @@ export * from './configuration_form'; export * from './document_fields'; + +export * from './templates_form'; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/index.ts new file mode 100644 index 0000000000000..a20841fab7783 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TemplatesForm } from './templates_form'; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form.tsx new file mode 100644 index 0000000000000..0aa6a90039a86 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form.tsx @@ -0,0 +1,126 @@ +/* + * 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, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui'; +import { useForm, Form, SerializerFunc, UseField, JsonEditorField } from '../../shared_imports'; +import { Types, useDispatch } from '../../mappings_state'; +import { templatesFormSchema } from './templates_form_schema'; +import { documentationService } from '../../../../services/documentation'; + +type MappingsTemplates = Types['MappingsTemplates']; + +interface Props { + defaultValue?: MappingsTemplates; +} + +const stringifyJson = (json: { [key: string]: any }) => + Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; + +const formSerializer: SerializerFunc = formData => { + const { dynamicTemplates } = formData; + + let parsedTemplates; + try { + parsedTemplates = JSON.parse(dynamicTemplates); + + if (!Array.isArray(parsedTemplates)) { + // User provided an object, but we need an array of objects + parsedTemplates = [parsedTemplates]; + } + } catch { + parsedTemplates = []; + } + + return { + dynamic_templates: parsedTemplates, + }; +}; + +const formDeserializer = (formData: { [key: string]: any }) => { + const { dynamic_templates } = formData; + + return { + dynamicTemplates: stringifyJson(dynamic_templates), + }; +}; + +export const TemplatesForm = React.memo(({ defaultValue }: Props) => { + const didMountRef = useRef(false); + + const { form } = useForm({ + schema: templatesFormSchema, + serializer: formSerializer, + deserializer: formDeserializer, + defaultValue, + }); + const dispatch = useDispatch(); + + useEffect(() => { + const subscription = form.subscribe(updatedTemplates => { + dispatch({ type: 'templates.update', value: { ...updatedTemplates, form } }); + }); + return subscription.unsubscribe; + }, [form]); + + useEffect(() => { + if (didMountRef.current) { + // If the defaultValue has changed (it probably means that we have loaded a new JSON) + // we need to reset the form to update the fields values. + form.reset({ resetValues: true }); + } else { + // Avoid reseting the form on component mount. + didMountRef.current = true; + } + }, [defaultValue]); + + useEffect(() => { + return () => { + // On unmount => save in the state a snapshot of the current form data. + dispatch({ type: 'templates.save' }); + }; + }, []); + + return ( + <> + + + {i18n.translate('xpack.idxMgmt.mappingsEditor.dynamicTemplatesDocumentationLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + +
+ + + + ); +}); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form_schema.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form_schema.ts new file mode 100644 index 0000000000000..667b5685723d2 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/templates_form/templates_form_schema.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { FormSchema, fieldValidators } from '../../shared_imports'; +import { MappingsTemplates } from '../../reducer'; + +const { isJsonField } = fieldValidators; + +export const templatesFormSchema: FormSchema = { + dynamicTemplates: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorLabel', { + defaultMessage: 'Dynamic templates data', + }), + validations: [ + { + validator: isJsonField( + i18n.translate('xpack.idxMgmt.mappingsEditor.templates.dynamicTemplatesEditorJsonError', { + defaultMessage: 'The dynamic templates JSON is not valid.', + }) + ), + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts index d1f11bfccae6a..569ffa92b81d0 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts @@ -243,7 +243,7 @@ export const validateMappings = (mappings: any = {}): MappingsValidatorResponse return { value: {} }; } - const { properties, ...mappingsConfiguration } = mappings; + const { properties, dynamic_templates, ...mappingsConfiguration } = mappings; const { value: parsedConfiguration, errors: configurationErrors } = validateMappingsConfiguration( mappingsConfiguration @@ -256,6 +256,7 @@ export const validateMappings = (mappings: any = {}): MappingsValidatorResponse value: { ...parsedConfiguration, properties: parsedProperties, + dynamic_templates, }, errors: errors.length ? errors : undefined, }; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx index 8c0bf1feb0ea5..6475ee9eb90b7 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_editor.tsx @@ -13,6 +13,7 @@ import { DocumentFieldsHeader, DocumentFields, DocumentFieldsJsonEditor, + TemplatesForm, } from './components'; import { IndexSettings } from './types'; import { State, Dispatch } from './reducer'; @@ -25,7 +26,7 @@ interface Props { indexSettings?: IndexSettings; } -type TabName = 'fields' | 'advanced'; +type TabName = 'fields' | 'advanced' | 'templates'; export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => { const [selectedTab, selectTab] = useState('fields'); @@ -39,6 +40,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting date_detection, dynamic_date_formats, properties = {}, + dynamic_templates, } = defaultValue ?? {}; return { @@ -51,6 +53,9 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting dynamic_date_formats, }, fields: properties, + templates: { + dynamic_templates, + }, }; }, [defaultValue]); @@ -66,6 +71,12 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting */ return; } + } else if (selectedTab === 'templates') { + const { isValid: isTemplatesFormValid } = await state.templates.form!.submit(); + + if (!isTemplatesFormValid) { + return; + } } selectTab(tab); @@ -82,16 +93,17 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting ); - const content = - selectedTab === 'fields' ? ( + const tabToContentMap = { + fields: ( <> {editor} - ) : ( - - ); + ), + advanced: , + templates: , + }; return (
@@ -104,6 +116,14 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting defaultMessage: 'Mapped fields', })} + changeTab('templates', [state, dispatch])} + isSelected={selectedTab === 'templates'} + > + {i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', { + defaultMessage: 'Dynamic templates', + })} + changeTab('advanced', [state, dispatch])} isSelected={selectedTab === 'advanced'} @@ -116,7 +136,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting - {content} + {tabToContentMap[selectedTab]}
); }} diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx index 965264bf53abb..2d098e91c774e 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/mappings_state.tsx @@ -11,20 +11,23 @@ import { addFieldToState, MappingsConfiguration, MappingsFields, + MappingsTemplates, State, Dispatch, } from './reducer'; import { Field, FieldsEditor } from './types'; import { normalize, deNormalize } from './lib'; -type Mappings = MappingsConfiguration & { - properties: MappingsFields; -}; +type Mappings = MappingsTemplates & + MappingsConfiguration & { + properties: MappingsFields; + }; export interface Types { Mappings: Mappings; MappingsConfiguration: MappingsConfiguration; MappingsFields: MappingsFields; + MappingsTemplates: MappingsTemplates; } export interface OnUpdateHandlerArg { @@ -45,7 +48,11 @@ export interface Props { editor: FieldsEditor; getProperties(): Mappings['properties']; }) => React.ReactNode; - defaultValue: { configuration: MappingsConfiguration; fields: { [key: string]: Field } }; + defaultValue: { + templates: MappingsTemplates; + configuration: MappingsConfiguration; + fields: { [key: string]: Field }; + }; onUpdate: OnUpdateHandler; } @@ -66,6 +73,14 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, validate: () => Promise.resolve(true), }, + templates: { + defaultValue: defaultValue.templates, + data: { + raw: defaultValue.templates, + format: () => defaultValue.templates, + }, + validate: () => Promise.resolve(true), + }, fields: parsedFieldsDefaultValue, documentFields: { status: 'idle', @@ -117,9 +132,11 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P : deNormalize(nextState.fields); const configurationData = nextState.configuration.data.format(); + const templatesData = nextState.templates.data.format(); return { ...configurationData, + ...templatesData, properties: fields, }; }, @@ -129,7 +146,12 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P ? (await state.configuration.form!.submit()).isValid : Promise.resolve(true); - const promisesToValidate = [configurationFormValidator]; + const templatesFormValidator = + state.templates.form !== undefined + ? (await state.templates.form!.submit()).isValid + : Promise.resolve(true); + + const promisesToValidate = [configurationFormValidator, templatesFormValidator]; if (state.fieldForm !== undefined && !bypassFieldFormValidation) { promisesToValidate.push(state.fieldForm.validate()); @@ -146,13 +168,14 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P useEffect(() => { /** * If the defaultValue has changed that probably means that we have loaded - * new data from JSON. We need to update our state witht the new mappings. + * new data from JSON. We need to update our state with the new mappings. */ if (didMountRef.current) { dispatch({ type: 'editor.replaceMappings', value: { configuration: defaultValue.configuration, + templates: defaultValue.templates, fields: parsedFieldsDefaultValue, }, }); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts index 59586db635d78..a2404b8106792 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/reducer.ts @@ -31,6 +31,14 @@ export interface MappingsConfiguration { _meta?: string; } +export interface MappingsTemplates { + dynamic_templates: Template[]; +} + +interface Template { + [key: string]: any; +} + export interface MappingsFields { [key: string]: any; } @@ -57,12 +65,20 @@ export interface State { format(): MappingsFields; isValid: boolean; }; + templates: { + defaultValue: { + dynamic_templates: MappingsTemplates['dynamic_templates']; + }; + form?: FormHook; + } & OnFormUpdateArg; } export type Action = | { type: 'editor.replaceMappings'; value: { [key: string]: any } } | { type: 'configuration.update'; value: Partial } | { type: 'configuration.save' } + | { type: 'templates.update'; value: Partial } + | { type: 'templates.save' } | { type: 'fieldForm.update'; value: OnFormUpdateArg } | { type: 'field.add'; value: Field } | { type: 'field.remove'; value: string } @@ -237,6 +253,10 @@ export const reducer = (state: State, action: Action): State => { ...state.configuration, defaultValue: action.value.configuration, }, + templates: { + ...state.templates, + defaultValue: action.value.templates, + }, documentFields: { ...state.documentFields, status: 'idle', @@ -274,6 +294,36 @@ export const reducer = (state: State, action: Action): State => { }, }; } + case 'templates.update': { + const nextState = { + ...state, + templates: { ...state.templates, ...action.value }, + }; + + const isValid = isStateValid(nextState); + nextState.isValid = isValid; + + return nextState; + } + case 'templates.save': { + const { + data: { raw, format }, + } = state.templates; + const templatesData = format(); + + return { + ...state, + templates: { + isValid: true, + defaultValue: templatesData, + data: { + raw, + format: () => templatesData, + }, + validate: async () => true, + }, + }; + } case 'fieldForm.update': { const nextState = { ...state, diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts index 7a52d4889c874..7aba268333853 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts @@ -19,12 +19,6 @@ export interface DataTypeDefinition { description?: () => ReactNode; } -export type ConfigType = - | 'dynamic' - | 'date_detection' - | 'numeric_detection' - | 'dynamic_date_formats'; - export type MainType = | 'text' | 'keyword' diff --git a/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts b/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts index bde9301827fb1..f90083ed72c40 100644 --- a/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts +++ b/x-pack/legacy/plugins/index_management/public/app/services/documentation.ts @@ -57,6 +57,10 @@ class DocumentationService { return `${this.esDocsBase}/mapping-meta-field.html`; } + public getDynamicTemplatesLink() { + return `${this.esDocsBase}/dynamic-templates.html`; + } + public getMappingSourceFieldLink() { return `${this.esDocsBase}/mapping-source-field.html`; }