mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Mappings editor] Add code improvements for source field (#201188)
Closes https://github.com/elastic/kibana/issues/200769 ## Summary This PR is a follow-up to https://github.com/elastic/kibana/pull/199854 and it adds the following code improvements: - Replaces Mappings-editor-context-level property `hasEnterpriceLicense` with plugin-context-level `canUseSyntheticSource` property - Adds jest tests to check if the synthetic option is correctly displayed based on license - Improves readability of serializer logic for the source field **How to test:** The same test instructions from https://github.com/elastic/kibana/pull/199854 can be followed with a focus on checking that the synthetic option is only available in Enterprise license.
This commit is contained in:
parent
e4d32fd527
commit
762bb7f59d
12 changed files with 93 additions and 92 deletions
|
@ -79,6 +79,7 @@ export interface AppDependencies {
|
|||
docLinks: DocLinksStart;
|
||||
kibanaVersion: SemVer;
|
||||
overlays: OverlayStart;
|
||||
canUseSyntheticSource: boolean;
|
||||
}
|
||||
|
||||
export const AppContextProvider = ({
|
||||
|
|
|
@ -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<TestSubjects>;
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => (
|
||||
<Fragment>
|
||||
|
@ -44,7 +44,7 @@ export const SourceFieldSection = () => {
|
|||
},
|
||||
];
|
||||
|
||||
if (state.hasEnterpriseLicense) {
|
||||
if (canUseSyntheticSource) {
|
||||
sourceValueOptions.push({
|
||||
value: SYNTHETIC_SOURCE_OPTION,
|
||||
inputDisplay: sourceOptionLabels[SYNTHETIC_SOURCE_OPTION],
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<MappingsEditorParsedMetadata>(
|
||||
() => 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: (
|
||||
|
|
|
@ -60,7 +60,6 @@ export const StateProvider: React.FC<{ children?: React.ReactNode }> = ({ childr
|
|||
selectedDataTypes: [],
|
||||
},
|
||||
inferenceToModelIdMap: {},
|
||||
hasEnterpriseLicense: false,
|
||||
mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 },
|
||||
};
|
||||
|
||||
|
|
|
@ -629,11 +629,5 @@ export const reducer = (state: State, action: Action): State => {
|
|||
inferenceToModelIdMap: action.value.inferenceToModelIdMap,
|
||||
};
|
||||
}
|
||||
case 'hasEnterpriseLicense.update': {
|
||||
return {
|
||||
...state,
|
||||
hasEnterpriseLicense: action.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<StartDependencies>;
|
||||
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 });
|
||||
|
|
|
@ -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<unknown> }) => {
|
||||
|
@ -213,5 +222,7 @@ export class IndexMgmtUIPlugin
|
|||
},
|
||||
};
|
||||
}
|
||||
public stop() {}
|
||||
public stop() {
|
||||
this.licensingSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue