From fda12bcc092a1d8fd200efc045d5208002243cf2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:34:18 +1100 Subject: [PATCH] [8.x] [Mappings editor] Add code improvements for source field (#201188) (#201577) # Backport This will backport the following commits from `main` to `8.x`: - [[Mappings editor] Add code improvements for source field (#201188)](https://github.com/elastic/kibana/pull/201188) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com> --- .../public/application/app_context.tsx | 1 + .../configuration_form.test.tsx | 48 +++++++++++++------ .../mappings_editor.test.tsx | 22 ++------- .../configuration_form/configuration_form.tsx | 45 +++++++++-------- .../source_field_section.tsx | 6 +-- .../mappings_editor/lib/utils.test.ts | 1 - .../mappings_editor/mappings_editor.tsx | 32 +++---------- .../mappings_state_context.tsx | 1 - .../components/mappings_editor/reducer.ts | 6 --- .../components/mappings_editor/types/state.ts | 4 +- .../application/mount_management_section.ts | 6 +++ .../plugins/index_management/public/plugin.ts | 13 ++++- 12 files changed, 93 insertions(+), 92 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 1a8276d76b7fc..6df01a109a036 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -79,6 +79,7 @@ export interface AppDependencies { docLinks: DocLinksStart; kibanaVersion: SemVer; overlays: OverlayStart; + canUseSyntheticSource: boolean; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx index 4c73fd1037dda..5bc28052b73e2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/configuration_form.test.tsx @@ -28,6 +28,14 @@ const setup = (props: any = { onUpdate() {} }, appDependencies?: any) => { return testBed; }; +const getContext = (sourceFieldEnabled: boolean = true, canUseSyntheticSource: boolean = true) => + ({ + config: { + enableMappingsSourceFieldSection: sourceFieldEnabled, + }, + canUseSyntheticSource, + } as unknown as AppDependencies); + describe('Mappings editor: configuration form', () => { let testBed: TestBed; @@ -49,14 +57,8 @@ describe('Mappings editor: configuration form', () => { describe('_source field', () => { it('renders the _source field when it is enabled', async () => { - const ctx = { - config: { - enableMappingsSourceFieldSection: true, - }, - } as unknown as AppDependencies; - await act(async () => { - testBed = setup({ esNodesPlugins: [] }, ctx); + testBed = setup({ esNodesPlugins: [] }, getContext()); }); testBed.component.update(); const { exists } = testBed; @@ -65,19 +67,37 @@ describe('Mappings editor: configuration form', () => { }); it("doesn't render the _source field when it is disabled", async () => { - const ctx = { - config: { - enableMappingsSourceFieldSection: false, - }, - } as unknown as AppDependencies; - await act(async () => { - testBed = setup({ esNodesPlugins: [] }, ctx); + testBed = setup({ esNodesPlugins: [] }, getContext(false)); }); testBed.component.update(); const { exists } = testBed; expect(exists('sourceField')).toBe(false); }); + + it('has synthetic option if `canUseSyntheticSource` is set to true', async () => { + await act(async () => { + testBed = setup({ esNodesPlugins: [] }, getContext(true, true)); + }); + testBed.component.update(); + const { exists, find } = testBed; + + // Clicking on the field to open the options dropdown + find('sourceValueField').simulate('click'); + expect(exists('syntheticSourceFieldOption')).toBe(true); + }); + + it("doesn't have synthetic option if `canUseSyntheticSource` is set to false", async () => { + await act(async () => { + testBed = setup({ esNodesPlugins: [] }, getContext(true, false)); + }); + testBed.component.update(); + const { exists, find } = testBed; + + // Clicking on the field to open the options dropdown + find('sourceValueField').simulate('click'); + expect(exists('syntheticSourceFieldOption')).toBe(false); + }); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index ee3b3e72e7c19..cb8a1efbf9c70 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -28,22 +28,9 @@ describe('Mappings editor: core', () => { let onChangeHandler: jest.Mock = jest.fn(); let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); let testBed: MappingsEditorTestBed; - let hasEnterpriseLicense = true; - const mockLicenseCheck = jest.fn((type: any) => hasEnterpriseLicense); const appDependencies = { plugins: { ml: { mlApi: {} }, - licensing: { - license$: { - subscribe: jest.fn((callback: any) => { - callback({ - isActive: true, - hasAtLeast: mockLicenseCheck, - }); - return { unsubscribe: jest.fn() }; - }), - }, - }, }, }; @@ -314,6 +301,7 @@ describe('Mappings editor: core', () => { config: { enableMappingsSourceFieldSection: true, }, + canUseSyntheticSource: true, ...appDependencies, }; @@ -512,8 +500,7 @@ describe('Mappings editor: core', () => { }); ['logsdb', 'time_series'].forEach((indexMode) => { - it(`defaults to 'synthetic' with ${indexMode} index mode prop on enterprise license`, async () => { - hasEnterpriseLicense = true; + it(`defaults to 'synthetic' with ${indexMode} index mode prop when 'canUseSyntheticSource' is set to true`, async () => { await act(async () => { testBed = setup( { @@ -537,8 +524,7 @@ describe('Mappings editor: core', () => { expect(find('sourceValueField').prop('value')).toBe('synthetic'); }); - it(`defaults to 'standard' with ${indexMode} index mode prop on basic license`, async () => { - hasEnterpriseLicense = false; + it(`defaults to 'standard' with ${indexMode} index mode prop when 'canUseSyntheticSource' is set to true`, async () => { await act(async () => { testBed = setup( { @@ -546,7 +532,7 @@ describe('Mappings editor: core', () => { onChange: onChangeHandler, indexMode, }, - ctx + { ...ctx, canUseSyntheticSource: false } ); }); testBed.component.update(); 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 00ce2d02a1baa..6ddaf8e48fe42 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 @@ -39,6 +39,31 @@ interface SerializedSourceField { excludes?: string[]; } +const serializeSourceField = (sourceField: any): SerializedSourceField | undefined => { + if (sourceField?.option === SYNTHETIC_SOURCE_OPTION) { + return { mode: SYNTHETIC_SOURCE_OPTION }; + } + if (sourceField?.option === DISABLED_SOURCE_OPTION) { + return { enabled: false }; + } + if (sourceField?.option === STORED_SOURCE_OPTION) { + return { + mode: 'stored', + includes: sourceField.includes, + excludes: sourceField.excludes, + }; + } + if (sourceField?.includes || sourceField?.excludes) { + // If sourceField?.option is undefined, the user hasn't explicitly selected + // this option, so don't include the `mode` property + return { + includes: sourceField.includes, + excludes: sourceField.excludes, + }; + } + return undefined; +}; + export const formSerializer = (formData: GenericObject) => { const { dynamicMapping, sourceField, metaField, _routing, _size, subobjects } = formData; @@ -48,30 +73,12 @@ export const formSerializer = (formData: GenericObject) => { ? 'strict' : dynamicMapping?.enabled; - const _source = - sourceField?.option === SYNTHETIC_SOURCE_OPTION - ? { mode: SYNTHETIC_SOURCE_OPTION } - : sourceField?.option === DISABLED_SOURCE_OPTION - ? { enabled: false } - : sourceField?.option === STORED_SOURCE_OPTION - ? { - mode: 'stored', - includes: sourceField?.includes, - excludes: sourceField?.excludes, - } - : 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: _source as SerializedSourceField, + _source: serializeSourceField(sourceField), _meta: metaField, _routing, _size, 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 2e8f9fb88f87d..c1709fa135035 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 @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut, EuiText } from '@elastic/eui'; -import { useMappingsState } from '../../../mappings_state_context'; +import { useAppContext } from '../../../../../app_context'; import { documentationService } from '../../../../../services/documentation'; import { UseField, FormDataProvider, FormRow, SuperSelectField } from '../../../shared_imports'; import { ComboBoxOption } from '../../../types'; @@ -24,7 +24,7 @@ import { } from './constants'; export const SourceFieldSection = () => { - const state = useMappingsState(); + const { canUseSyntheticSource } = useAppContext(); const renderOptionDropdownDisplay = (option: SourceOptionKey) => ( @@ -44,7 +44,7 @@ export const SourceFieldSection = () => { }, ]; - if (state.hasEnterpriseLicense) { + if (canUseSyntheticSource) { sourceValueOptions.push({ value: SYNTHETIC_SOURCE_OPTION, inputDisplay: sourceOptionLabels[SYNTHETIC_SOURCE_OPTION], diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 872c62bc6f7a7..58b40293f64f2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -421,7 +421,6 @@ describe('utils', () => { selectedDataTypes: ['Boolean'], }, inferenceToModelIdMap: {}, - hasEnterpriseLicense: true, mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 }, }; test('returns list of matching fields with search term', () => { 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 cc87c3cd614e3..9f59f4959bed5 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,7 +9,6 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; -import { ILicense } from '@kbn/licensing-plugin/common/types'; import { useAppContext } from '../../app_context'; import { IndexMode } from '../../../../common/types/data_streams'; import { @@ -61,9 +60,7 @@ export interface Props { export const MappingsEditor = React.memo( ({ onChange, value, docLinks, indexSettings, esNodesPlugins, indexMode }: Props) => { - const { - plugins: { licensing }, - } = useAppContext(); + const { canUseSyntheticSource } = useAppContext(); const { parsedDefaultValue, multipleMappingsDeclared } = useMemo( () => parseMappings(value), [value] @@ -128,39 +125,22 @@ export const MappingsEditor = React.memo( [dispatch] ); - const [isLicenseCheckComplete, setIsLicenseCheckComplete] = useState(false); - useEffect(() => { - const subscription = licensing?.license$.subscribe((license: ILicense) => { - dispatch({ - type: 'hasEnterpriseLicense.update', - value: license.isActive && license.hasAtLeast('enterprise'), - }); - setIsLicenseCheckComplete(true); - }); - - return () => subscription?.unsubscribe(); - }, [dispatch, licensing]); - useEffect(() => { if ( - isLicenseCheckComplete && !state.configuration.defaultValue._source && (indexMode === LOGSDB_INDEX_MODE || indexMode === TIME_SERIES_MODE) ) { - if (state.hasEnterpriseLicense) { + if (canUseSyntheticSource) { + // If the source field is undefined (hasn't been set in the form) + // and if the user has selected a `logsdb` or `time_series` index mode in the Logistics step, + // update the form data with synthetic _source dispatch({ type: 'configuration.save', value: { ...state.configuration.defaultValue, _source: { mode: 'synthetic' } } as any, }); } } - }, [ - indexMode, - dispatch, - state.configuration, - state.hasEnterpriseLicense, - isLicenseCheckComplete, - ]); + }, [indexMode, dispatch, state.configuration, canUseSyntheticSource]); const tabToContentMap = { fields: ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx index 4fd89ad0e25ad..ac19c5395f974 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx @@ -60,7 +60,6 @@ export const StateProvider: React.FC<{ children?: React.ReactNode }> = ({ childr selectedDataTypes: [], }, inferenceToModelIdMap: {}, - hasEnterpriseLicense: false, mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 }, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index ecb9648c34d00..626ee0e839a8a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -629,11 +629,5 @@ export const reducer = (state: State, action: Action): State => { inferenceToModelIdMap: action.value.inferenceToModelIdMap, }; } - case 'hasEnterpriseLicense.update': { - return { - ...state, - hasEnterpriseLicense: action.value, - }; - } } }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts index 43b3a7dde3b16..f40fe420eb3be 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts @@ -108,7 +108,6 @@ export interface State { }; templates: TemplatesFormState; inferenceToModelIdMap?: InferenceToModelIdMap; - hasEnterpriseLicense: boolean; mappingViewFields: NormalizedFields; // state of the incoming index mappings, separate from the editor state above } @@ -141,7 +140,6 @@ export type Action = | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } | { type: 'search:update'; value: string } | { type: 'validity:update'; value: boolean } - | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } } - | { type: 'hasEnterpriseLicense.update'; value: boolean }; + | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } }; export type Dispatch = (action: Action) => void; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index da17bc4706c02..48a45579a01fc 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -57,6 +57,7 @@ export function getIndexManagementDependencies({ cloud, startDependencies, uiMetricService, + canUseSyntheticSource, }: { core: CoreStart; usageCollection: UsageCollectionSetup; @@ -68,6 +69,7 @@ export function getIndexManagementDependencies({ cloud?: CloudSetup; startDependencies: StartDependencies; uiMetricService: UiMetricService; + canUseSyntheticSource: boolean; }): AppDependencies { const { docLinks, application, uiSettings, settings } = core; const { url } = startDependencies.share; @@ -100,6 +102,7 @@ export function getIndexManagementDependencies({ docLinks, kibanaVersion, overlays: core.overlays, + canUseSyntheticSource, }; } @@ -112,6 +115,7 @@ export async function mountManagementSection({ kibanaVersion, config, cloud, + canUseSyntheticSource, }: { coreSetup: CoreSetup; usageCollection: UsageCollectionSetup; @@ -121,6 +125,7 @@ export async function mountManagementSection({ kibanaVersion: SemVer; config: AppDependencies['config']; cloud?: CloudSetup; + canUseSyntheticSource: boolean; }) { const { element, setBreadcrumbs, history } = params; const [core, startDependencies] = await coreSetup.getStartServices(); @@ -148,6 +153,7 @@ export async function mountManagementSection({ startDependencies, uiMetricService, usageCollection, + canUseSyntheticSource, }); const unmountAppCallback = renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 5d857d8d8ac9c..82ba6505696aa 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -20,6 +20,7 @@ import { IndexManagementPluginStart, } from '@kbn/index-management-shared-types'; import { IndexManagementLocator } from '@kbn/index-management-shared-types'; +import { Subscription } from 'rxjs'; import { setExtensionsService } from './application/store/selectors/extension_service'; import { ExtensionsService } from './services/extensions_service'; @@ -57,6 +58,8 @@ export class IndexMgmtUIPlugin enableProjectLevelRetentionChecks: boolean; enableSemanticText: boolean; }; + private canUseSyntheticSource: boolean = false; + private licensingSubscription?: Subscription; constructor(ctx: PluginInitializerContext) { // Temporary hack to provide the service instances in module files in order to avoid a big refactor @@ -113,6 +116,7 @@ export class IndexMgmtUIPlugin kibanaVersion: this.kibanaVersion, config: this.config, cloud, + canUseSyntheticSource: this.canUseSyntheticSource, }); }, }); @@ -133,6 +137,11 @@ export class IndexMgmtUIPlugin public start(coreStart: CoreStart, plugins: StartDependencies): IndexManagementPluginStart { const { fleet, usageCollection, cloud, share, console, ml, licensing } = plugins; + + this.licensingSubscription = licensing?.license$.subscribe((next) => { + this.canUseSyntheticSource = next.hasAtLeast('enterprise'); + }); + return { extensionsService: this.extensionsService.setup(), getIndexMappingComponent: (deps: { history: ScopedHistory }) => { @@ -213,5 +222,7 @@ export class IndexMgmtUIPlugin }, }; } - public stop() {} + public stop() { + this.licensingSubscription?.unsubscribe(); + } }