From b234d7af8ae4f18931423069e53c943dc5ffca58 Mon Sep 17 00:00:00 2001 From: Elena Stoeva Date: Tue, 12 Nov 2024 18:17:16 +0000 Subject: [PATCH] [Mappings Editor] Add support for synthetic _source --- .../configuration_form/configuration_form.tsx | 43 +++--- .../configuration_form_schema.tsx | 18 +-- .../source_field_section/constants.ts | 10 ++ .../source_field_section/i18n_texts.ts | 51 +++++++ .../source_field_section/index.ts | 1 + .../source_field_section.tsx | 124 ++++++++++++++++-- .../mappings_editor/mappings_editor.tsx | 5 +- .../components/wizard_steps/step_mappings.tsx | 5 +- .../wizard_steps/step_mappings_container.tsx | 15 ++- .../template_form/template_form.tsx | 5 +- .../application/services/documentation.ts | 6 + 11 files changed, 243 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index e3e32c55aada0..49d2f6d69458e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -5,30 +5,36 @@ * 2.0. */ -import React, { useEffect, useRef, useCallback } from 'react'; +import React, { useEffect, useRef } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { FormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { useAppContext } from '../../../../app_context'; import { useForm, Form } from '../../shared_imports'; import { GenericObject, MappingsConfiguration } from '../../types'; import { MapperSizePluginId } from '../../constants'; import { useDispatch } from '../../mappings_state_context'; import { DynamicMappingSection } from './dynamic_mapping_section'; -import { SourceFieldSection } from './source_field_section'; +import { + SourceFieldSection, + STORED_SOURCE_OPTION, + SYNTHETIC_SOURCE_OPTION, + DISABLED_SOURCE_OPTION, +} from './source_field_section'; import { MetaFieldSection } from './meta_field_section'; import { RoutingSection } from './routing_section'; import { MapperSizePluginSection } from './mapper_size_plugin_section'; import { SubobjectsSection } from './subobjects_section'; import { configurationFormSchema } from './configuration_form_schema'; +import { IndexMode } from '../../../../../../common/types/data_streams'; interface Props { value?: MappingsConfiguration; /** List of plugins installed in the cluster nodes */ esNodesPlugins: string[]; + indexMode?: IndexMode; } -const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => { +const formSerializer = (formData: GenericObject) => { const { dynamicMapping, sourceField, metaField, _routing, _size, subobjects } = formData; const dynamic = dynamicMapping?.enabled @@ -37,12 +43,21 @@ const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => { ? 'strict' : dynamicMapping?.enabled; + const _source = + sourceField?.option === SYNTHETIC_SOURCE_OPTION + ? { mode: SYNTHETIC_SOURCE_OPTION } + : sourceField?.option === DISABLED_SOURCE_OPTION + ? { enabled: false } + : sourceField?.includes || sourceField?.excludes + ? { includes: sourceField?.includes, excludes: sourceField?.excludes } + : undefined; + const serialized = { dynamic, numeric_detection: dynamicMapping?.numeric_detection, date_detection: dynamicMapping?.date_detection, dynamic_date_formats: dynamicMapping?.dynamic_date_formats, - _source: sourceFieldMode ? { mode: sourceFieldMode } : sourceField, + _source, _meta: metaField, _routing, _size, @@ -60,8 +75,9 @@ const formDeserializer = (formData: GenericObject) => { date_detection, dynamic_date_formats, /* eslint-enable @typescript-eslint/naming-convention */ - _source: { enabled, includes, excludes } = {} as { + _source: { enabled, mode, includes, excludes } = {} as { enabled?: boolean; + mode?: string; includes?: string[]; excludes?: string[]; }, @@ -81,7 +97,7 @@ const formDeserializer = (formData: GenericObject) => { dynamic_date_formats, }, sourceField: { - enabled, + option: mode ?? (enabled === false ? 'disabled' : 'stored'), includes, excludes, }, @@ -92,21 +108,16 @@ const formDeserializer = (formData: GenericObject) => { }; }; -export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) => { +export const ConfigurationForm = React.memo(({ value, esNodesPlugins, indexMode }: Props) => { const { config: { enableMappingsSourceFieldSection }, } = useAppContext(); const isMounted = useRef(false); - const serializerCallback = useCallback( - (formData: FormData) => formSerializer(formData, value?._source?.mode), - [value?._source?.mode] - ); - const { form } = useForm({ - schema: configurationFormSchema, - serializer: serializerCallback, + schema: configurationFormSchema(indexMode), + serializer: formSerializer, deserializer: formDeserializer, defaultValue: value, id: 'configurationForm', @@ -165,7 +176,7 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) = - {enableMappingsSourceFieldSection && !value?._source?.mode && ( + {enableMappingsSourceFieldSection && ( <> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index a9913b9474b36..8e607c539f0b9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -14,6 +14,8 @@ import { EuiLink, EuiCode } from '@elastic/eui'; import { documentationService } from '../../../../services/documentation'; import { FormSchema, FIELD_TYPES, fieldValidators } from '../../shared_imports'; import { ComboBoxOption } from '../../types'; +import { STORED_SOURCE_OPTION, SYNTHETIC_SOURCE_OPTION } from './source_field_section'; +import { IndexMode } from "@kbn/index-management-plugin/common/types/data_streams"; const { isJsonField } = fieldValidators; @@ -30,7 +32,7 @@ const fieldPathComboBoxConfig = { deserializer: (values: string[]): ComboBoxOption[] => values.map((value) => ({ label: value })), }; -export const configurationFormSchema: FormSchema = { +export const configurationFormSchema = (indexMode?: IndexMode): FormSchema => ({ metaField: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.metaFieldEditorLabel', { defaultMessage: '_meta field data', @@ -75,12 +77,12 @@ export const configurationFormSchema: FormSchema = { }, }, sourceField: { - enabled: { - label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.sourceFieldLabel', { - defaultMessage: 'Enable _source field', - }), - type: FIELD_TYPES.TOGGLE, - defaultValue: true, + option: { + type: FIELD_TYPES.SUPER_SELECT, + defaultValue: + indexMode === 'logsdb' || indexMode === 'time_series' + ? SYNTHETIC_SOURCE_OPTION + : STORED_SOURCE_OPTION, }, includes: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.includeSourceFieldsLabel', { @@ -192,4 +194,4 @@ export const configurationFormSchema: FormSchema = { }), defaultValue: true, }, -}; +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts new file mode 100644 index 0000000000000..ad5755dd45f9d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const STORED_SOURCE_OPTION = 'stored'; +export const DISABLED_SOURCE_OPTION = 'disabled'; +export const SYNTHETIC_SOURCE_OPTION = 'synthetic'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts new file mode 100644 index 0000000000000..64e04e15ad1ef --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { STORED_SOURCE_OPTION, DISABLED_SOURCE_OPTION, SYNTHETIC_SOURCE_OPTION} from './constants'; + +export const sourceOptionLabels = { + [STORED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.storedSourceFieldsLabel', + { + defaultMessage: 'Stored _source', + } + ), + [DISABLED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.disabledSourceFieldsLabel', + { + defaultMessage: 'Disabled _source', + } + ), + [SYNTHETIC_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.syntheticSourceFieldsLabel', + { + defaultMessage: 'Synthetic _source', + } + ), +}; + +export const sourceOptionDescriptions = { + [STORED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.storedSourceFieldsDescription', + { + defaultMessage: 'Stores content in _source field for future retrieval', + } + ), + [DISABLED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.disabledSourceFieldsDescription', + { + defaultMessage: 'Reconstructs source content to save on disk usage', + } + ), + [SYNTHETIC_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.syntheticSourceFieldsDescription', + { + defaultMessage: 'Strongly discouraged, will impact downstream functionality', + } + ), +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts index df921a036c909..8599c724f4b2e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts @@ -6,3 +6,4 @@ */ export { SourceFieldSection } from './source_field_section'; +export * from './constants'; \ No newline at end of file diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index 236dfe98119ca..3b5d62a5aeb62 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -5,18 +5,65 @@ * 2.0. */ -import React from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut } from '@elastic/eui'; +import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut, EuiText } from '@elastic/eui'; +import { ILicense } from '@kbn/licensing-plugin/common/types'; +import { useAppContext } from '../../../../../app_context'; import { documentationService } from '../../../../../services/documentation'; -import { UseField, FormDataProvider, FormRow, ToggleField } from '../../../shared_imports'; +import { UseField, FormDataProvider, FormRow, SuperSelectField } from '../../../shared_imports'; import { ComboBoxOption } from '../../../types'; +import { sourceOptionLabels, sourceOptionDescriptions } from './i18n_texts'; +import { STORED_SOURCE_OPTION, DISABLED_SOURCE_OPTION, SYNTHETIC_SOURCE_OPTION } from './constants'; export const SourceFieldSection = () => { - const renderWarning = () => ( + const { + plugins: { licensing }, + } = useAppContext(); + + const [isEnterpriseLicense, setIsEnterpriseLicense] = useState(false); + useEffect(() => { + const subscription = licensing?.license$.subscribe((license: ILicense) => { + setIsEnterpriseLicense(license.isActive && license.hasAtLeast('enterprise')); + }); + + return () => subscription?.unsubscribe(); + }, [licensing]); + + const renderOptionDropdownDisplay = (option) => ( + + {sourceOptionLabels[option]} + +

{sourceOptionDescriptions[option]}

+
+
+ ); + + const sourceValueOptions = [ + { + value: STORED_SOURCE_OPTION, + inputDisplay: sourceOptionLabels[STORED_SOURCE_OPTION], + dropdownDisplay: renderOptionDropdownDisplay(STORED_SOURCE_OPTION), + }, + ]; + + if (isEnterpriseLicense) { + sourceValueOptions.push({ + value: SYNTHETIC_SOURCE_OPTION, + inputDisplay: sourceOptionLabels[SYNTHETIC_SOURCE_OPTION], + dropdownDisplay: renderOptionDropdownDisplay(SYNTHETIC_SOURCE_OPTION), + }); + } + sourceValueOptions.push({ + value: DISABLED_SOURCE_OPTION, + inputDisplay: sourceOptionLabels[DISABLED_SOURCE_OPTION], + dropdownDisplay: renderOptionDropdownDisplay(DISABLED_SOURCE_OPTION), + }); + + const renderDisableWarning = () => ( {

@@ -45,13 +92,13 @@ export const SourceFieldSection = () => {

@@ -70,6 +117,44 @@ export const SourceFieldSection = () => { ); + const renderSyntheticWarning = () => ( + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText', + { + defaultMessage: '_source', + } + )} + + ), + learnMoreLink: ( + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + } + iconType="warning" + color="warning" + /> + ); + const renderFormFields = () => (

@@ -155,21 +240,36 @@ export const SourceFieldSection = () => { }} /> - + } > - + {(formData) => { const { - sourceField: { enabled }, + sourceField: { option }, } = formData; - if (enabled === undefined) { + if (option === undefined) { return null; } - return enabled ? renderFormFields() : renderWarning(); + return option === STORED_SOURCE_OPTION + ? renderFormFields() + : option === DISABLED_SOURCE_OPTION + ? renderDisableWarning() + : renderSyntheticWarning(); }} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index e1f5306899db3..3fd4c22b42a25 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; +import { IndexMode } from '../../../../common/types/data_streams'; import { DocumentFields, RuntimeFieldsList, @@ -52,10 +53,11 @@ export interface Props { docLinks: DocLinksStart; /** List of plugins installed in the cluster nodes */ esNodesPlugins: string[]; + indexMode?: IndexMode; } export const MappingsEditor = React.memo( - ({ onChange, value, docLinks, indexSettings, esNodesPlugins }: Props) => { + ({ onChange, value, docLinks, indexSettings, esNodesPlugins, indexMode }: Props) => { const { parsedDefaultValue, multipleMappingsDeclared } = useMemo( () => parseMappings(value), [value] @@ -148,6 +150,7 @@ export const MappingsEditor = React.memo( ), }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx index 62685a05f7ff9..a239971c1bf82 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx @@ -16,6 +16,7 @@ import { EuiText, } from '@elastic/eui'; +import { IndexMode } from '../../../../../../common/types/data_streams'; import { Forms } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; import { @@ -33,10 +34,11 @@ interface Props { esNodesPlugins: string[]; defaultValue?: { [key: string]: any }; indexSettings?: IndexSettings; + indexMode?: IndexMode; } export const StepMappings: React.FunctionComponent = React.memo( - ({ defaultValue = {}, onChange, indexSettings, esDocsBase, esNodesPlugins }) => { + ({ defaultValue = {}, onChange, indexSettings, esDocsBase, esNodesPlugins, indexMode }) => { const [mappings, setMappings] = useState(defaultValue); const { docLinks } = useAppContext(); @@ -115,6 +117,7 @@ export const StepMappings: React.FunctionComponent = React.memo( indexSettings={indexSettings} docLinks={docLinks} esNodesPlugins={esNodesPlugins} + indexMode={indexMode} /> diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 50b763ce0d06a..71316cbf90089 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -7,6 +7,8 @@ import React from 'react'; +import { WizardContent } from '../../../template_form/template_form'; +import { TemplateDeserialized } from '../../../../../../common'; import { Forms } from '../../../../../shared_imports'; import { useLoadNodesPlugins } from '../../../../services'; import { CommonWizardSteps } from './types'; @@ -14,15 +16,25 @@ import { StepMappings } from './step_mappings'; interface Props { esDocsBase: string; + getTemplateData: (wizardContent: WizardContent) => TemplateDeserialized; } -export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { +export const StepMappingsContainer: React.FunctionComponent = ({ + esDocsBase, + getTemplateData, +}) => { const { defaultValue, updateContent, getSingleContentData } = Forms.useContent< CommonWizardSteps, 'mappings' >('mappings'); const { data: esNodesPlugins } = useLoadNodesPlugins(); + const { getData } = Forms.useMultiContentContext(); + + const wizardContent = getData(); + // Build the current template object, providing the wizard content data + const template = getTemplateData(wizardContent); + return ( = ({ esDocsBa indexSettings={getSingleContentData('settings')} esDocsBase={esDocsBase} esNodesPlugins={esNodesPlugins ?? []} + indexMode={template?.indexMode} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 2da3eef609a65..4b5e3eb32b7bd 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -345,7 +345,10 @@ export const TemplateForm = ({ - + diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index 58aba69351883..62b7defd78db1 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -55,6 +55,7 @@ class DocumentationService { private mappingSimilarity: string = ''; private mappingSourceFields: string = ''; private mappingSourceFieldsDisable: string = ''; + private mappingSyntheticSourceFields: string = ''; private mappingStore: string = ''; private mappingSubobjects: string = ''; private mappingTermVector: string = ''; @@ -115,6 +116,7 @@ class DocumentationService { this.mappingSimilarity = links.elasticsearch.mappingSimilarity; this.mappingSourceFields = links.elasticsearch.mappingSourceFields; this.mappingSourceFieldsDisable = links.elasticsearch.mappingSourceFieldsDisable; + this.mappingSyntheticSourceFields = links.elasticsearch.mappingSyntheticSourceFields; this.mappingStore = links.elasticsearch.mappingStore; this.mappingSubobjects = links.elasticsearch.mappingSubobjects; this.mappingTermVector = links.elasticsearch.mappingTermVector; @@ -215,6 +217,10 @@ class DocumentationService { return this.mappingSourceFieldsDisable; } + public getMappingSyntheticSourceFieldLink() { + return this.mappingSyntheticSourceFields; + } + public getNullValueLink() { return this.mappingNullValue; }