Skip to content

Commit

Permalink
[Mappings Editor] Add support for synthetic _source (elastic#199854)
Browse files Browse the repository at this point in the history
Closes elastic#198621

## Summary

This PR adds support for the synthetic _source field in the mappings
Advanced options.

Stored option:
<img width="1184" alt="Screenshot 2024-11-14 at 19 19 19"
src="https://github.com/user-attachments/assets/086f7a3e-9ca1-42de-9f9c-d3599d839ccf">

Synthetic option selected:
<img width="1184" alt="Screenshot 2024-11-14 at 19 19 27"
src="https://github.com/user-attachments/assets/3700bced-212a-4378-b51a-ab7a0f4f7b99">

Disabled option selected:
<img width="1184" alt="Screenshot 2024-11-14 at 19 19 36"
src="https://github.com/user-attachments/assets/c7ddcbae-7c78-4477-824e-99b144a1f750">




https://github.com/user-attachments/assets/399d0f95-a5dd-4874-bb8c-e95d6ed38465




How to test:
1. Start Es with `yarn es snapshot --license` (we need Enterprise
license to see the Synthetic source option) and Kibana with `yarn start`
2. Go to Index templates/Component templates and start creating a
template
3. At the Mappings step, go to Advanced options.
4. Verify that selecting a _source field option translates to the
correct Es request.
5. In Index templates form, verify that the default _source option
depends on the index mode selected in the Logistics step. For LogsDB and
Time series index mode, the default should be synthetic mode; otherwise,
the stored option.
6. Verify that in Basic license, the synthetic option is not displayed.



### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
2 people authored and paulinashakirova committed Nov 26, 2024
1 parent 94006a4 commit c7949b9
Show file tree
Hide file tree
Showing 22 changed files with 562 additions and 51 deletions.
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
mappingSimilarity: `${ELASTICSEARCH_DOCS}similarity.html`,
mappingSourceFields: `${ELASTICSEARCH_DOCS}mapping-source-field.html`,
mappingSourceFieldsDisable: `${ELASTICSEARCH_DOCS}mapping-source-field.html#disable-source-field`,
mappingSyntheticSourceFields: `${ELASTICSEARCH_DOCS}mapping-source-field.html#synthetic-source`,
mappingStore: `${ELASTICSEARCH_DOCS}mapping-store.html`,
mappingSubobjects: `${ELASTICSEARCH_DOCS}subobjects.html`,
mappingTermVector: `${ELASTICSEARCH_DOCS}term-vector.html`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ export type TestSubjects =
| 'advancedConfiguration.dynamicMappingsToggle.input'
| 'advancedConfiguration.metaField'
| 'advancedConfiguration.routingRequiredToggle.input'
| 'sourceValueField'
| 'sourceField.includesField'
| 'sourceField.excludesField'
| 'dynamicTemplatesEditor'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,24 @@ describe('Mappings editor: core', () => {
let onChangeHandler: jest.Mock = jest.fn();
let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler);
let testBed: MappingsEditorTestBed;
const appDependencies = { plugins: { ml: { mlApi: {} } } };
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() };
}),
},
},
},
};

beforeAll(() => {
jest.useFakeTimers({ legacyFakeTimers: true });
Expand Down Expand Up @@ -456,13 +473,96 @@ describe('Mappings editor: core', () => {
updatedMappings = {
...updatedMappings,
dynamic: false,
// The "enabled": true is removed as this is the default in Es
_source: {
includes: defaultMappings._source.includes,
excludes: defaultMappings._source.excludes,
},
};
delete updatedMappings.date_detection;
delete updatedMappings.dynamic_date_formats;
delete updatedMappings.numeric_detection;

expect(data).toEqual(updatedMappings);
});

describe('props.indexMode sets the correct default value of _source field', () => {
it("defaults to 'stored' with 'standard' index mode prop", async () => {
await act(async () => {
testBed = setup(
{
value: { ...defaultMappings, _source: undefined },
onChange: onChangeHandler,
indexMode: 'standard',
},
ctx
);
});
testBed.component.update();

const {
actions: { selectTab },
find,
} = testBed;

await selectTab('advanced');

// Check that the stored option is selected
expect(find('sourceValueField').prop('value')).toBe('stored');
});

['logsdb', 'time_series'].forEach((indexMode) => {
it(`defaults to 'synthetic' with ${indexMode} index mode prop on enterprise license`, async () => {
hasEnterpriseLicense = true;
await act(async () => {
testBed = setup(
{
value: { ...defaultMappings, _source: undefined },
onChange: onChangeHandler,
indexMode,
},
ctx
);
});
testBed.component.update();

const {
actions: { selectTab },
find,
} = testBed;

await selectTab('advanced');

// Check that the synthetic option is selected
expect(find('sourceValueField').prop('value')).toBe('synthetic');
});

it(`defaults to 'standard' with ${indexMode} index mode prop on basic license`, async () => {
hasEnterpriseLicense = false;
await act(async () => {
testBed = setup(
{
value: { ...defaultMappings, _source: undefined },
onChange: onChangeHandler,
indexMode,
},
ctx
);
});
testBed.component.update();

const {
actions: { selectTab },
find,
} = testBed;

await selectTab('advanced');

// Check that the stored option is selected
expect(find('sourceValueField').prop('value')).toBe('stored');
});
});
});
});

describe('multi-fields support', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
* 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';
Expand All @@ -28,7 +32,14 @@ interface Props {
esNodesPlugins: string[];
}

const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => {
interface SerializedSourceField {
enabled?: boolean;
mode?: string;
includes?: string[];
excludes?: string[];
}

export const formSerializer = (formData: GenericObject) => {
const { dynamicMapping, sourceField, metaField, _routing, _size, subobjects } = formData;

const dynamic = dynamicMapping?.enabled
Expand All @@ -37,12 +48,30 @@ 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?.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: sourceFieldMode ? { mode: sourceFieldMode } : sourceField,
_source: _source as SerializedSourceField,
_meta: metaField,
_routing,
_size,
Expand All @@ -52,19 +81,15 @@ const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => {
return serialized;
};

const formDeserializer = (formData: GenericObject) => {
export const formDeserializer = (formData: GenericObject) => {
const {
dynamic,
/* eslint-disable @typescript-eslint/naming-convention */
numeric_detection,
date_detection,
dynamic_date_formats,
/* eslint-enable @typescript-eslint/naming-convention */
_source: { enabled, includes, excludes } = {} as {
enabled?: boolean;
includes?: string[];
excludes?: string[];
},
_source: { enabled, mode, includes, excludes } = {} as SerializedSourceField,
_meta,
_routing,
// For the Mapper Size plugin
Expand All @@ -81,7 +106,14 @@ const formDeserializer = (formData: GenericObject) => {
dynamic_date_formats,
},
sourceField: {
enabled,
option:
mode === 'stored'
? STORED_SOURCE_OPTION
: mode === 'synthetic'
? SYNTHETIC_SOURCE_OPTION
: enabled === false
? DISABLED_SOURCE_OPTION
: undefined,
includes,
excludes,
},
Expand All @@ -99,14 +131,9 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) =

const isMounted = useRef(false);

const serializerCallback = useCallback(
(formData: FormData) => formSerializer(formData, value?._source?.mode),
[value?._source?.mode]
);

const { form } = useForm({
schema: configurationFormSchema,
serializer: serializerCallback,
serializer: formSerializer,
deserializer: formDeserializer,
defaultValue: value,
id: 'configurationForm',
Expand Down Expand Up @@ -165,7 +192,7 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) =
<EuiSpacer size="xl" />
<MetaFieldSection />
<EuiSpacer size="xl" />
{enableMappingsSourceFieldSection && !value?._source?.mode && (
{enableMappingsSourceFieldSection && (
<>
<SourceFieldSection /> <EuiSpacer size="xl" />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,9 @@ 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: 'stored',
},
includes: {
label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.includeSourceFieldsLabel', {
Expand Down
Loading

0 comments on commit c7949b9

Please sign in to comment.