mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
7c575de914
commit
0b85d28b2b
14 changed files with 468 additions and 65 deletions
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* 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 { setup as mappingsEditorSetup } from './mappings_editor.helpers';
|
||||||
|
|
||||||
|
export {
|
||||||
|
nextTick,
|
||||||
|
getRandomString,
|
||||||
|
findTestSubject,
|
||||||
|
TestBed,
|
||||||
|
} from '../../../../../../../../../../test_utils';
|
||||||
|
|
||||||
|
export const componentHelpers = {
|
||||||
|
mappingsEditor: { setup: mappingsEditorSetup },
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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 { registerTestBed } from '../../../../../../../../../../test_utils';
|
||||||
|
import { MappingsEditor } from '../../../mappings_editor';
|
||||||
|
|
||||||
|
export const setup = (props: any) =>
|
||||||
|
registerTestBed(MappingsEditor, {
|
||||||
|
memoryRouter: {
|
||||||
|
wrapComponent: false,
|
||||||
|
},
|
||||||
|
defaultProps: props,
|
||||||
|
});
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 { componentHelpers } from './helpers';
|
||||||
|
|
||||||
|
const { setup } = componentHelpers.mappingsEditor;
|
||||||
|
const mockOnUpdate = () => undefined;
|
||||||
|
|
||||||
|
describe('<MappingsEditor />', () => {
|
||||||
|
describe('multiple mappings detection', () => {
|
||||||
|
test('should show a warning when multiple mappings are detected', async () => {
|
||||||
|
const defaultValue = {
|
||||||
|
type1: {
|
||||||
|
properties: {
|
||||||
|
name1: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type2: {
|
||||||
|
properties: {
|
||||||
|
name2: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const testBed = await setup({ onUpdate: mockOnUpdate, defaultValue })();
|
||||||
|
const { exists } = testBed;
|
||||||
|
|
||||||
|
expect(exists('mappingsEditor')).toBe(true);
|
||||||
|
expect(exists('mappingTypesDetectedCallout')).toBe(true);
|
||||||
|
expect(exists('documentFields')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not show a warning when mappings a single-type', async () => {
|
||||||
|
const defaultValue = {
|
||||||
|
properties: {
|
||||||
|
name1: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const testBed = await setup({ onUpdate: mockOnUpdate, defaultValue })();
|
||||||
|
const { exists } = testBed;
|
||||||
|
|
||||||
|
expect(exists('mappingsEditor')).toBe(true);
|
||||||
|
expect(exists('mappingTypesDetectedCallout')).toBe(false);
|
||||||
|
expect(exists('documentFields')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -48,7 +48,7 @@ export const DocumentFields = React.memo(() => {
|
||||||
const searchTerm = search.term.trim();
|
const searchTerm = search.term.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div data-test-subj="documentFields">
|
||||||
<DocumentFieldsHeader searchValue={search.term} onSearchChange={onSearchChange} />
|
<DocumentFieldsHeader searchValue={search.term} onSearchChange={onSearchChange} />
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
{searchTerm !== '' ? (
|
{searchTerm !== '' ? (
|
||||||
|
@ -57,6 +57,6 @@ export const DocumentFields = React.memo(() => {
|
||||||
editor
|
editor
|
||||||
)}
|
)}
|
||||||
{renderEditField()}
|
{renderEditField()}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,3 +9,5 @@ export * from './configuration_form';
|
||||||
export * from './document_fields';
|
export * from './document_fields';
|
||||||
|
|
||||||
export * from './templates_form';
|
export * from './templates_form';
|
||||||
|
|
||||||
|
export * from './multiple_mappings_warning';
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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 from 'react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { EuiCallOut, EuiLink } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { documentationService } from '../../../services/documentation';
|
||||||
|
|
||||||
|
export const MultipleMappingsWarning = () => (
|
||||||
|
<EuiCallOut
|
||||||
|
title={i18n.translate('xpack.idxMgmt.mappingsEditor.mappingTypesDetectedCallOutTitle', {
|
||||||
|
defaultMessage: 'Mapping types detected',
|
||||||
|
})}
|
||||||
|
iconType="alert"
|
||||||
|
color="warning"
|
||||||
|
data-test-subj="mappingTypesDetectedCallout"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.idxMgmt.mappingsEditor.mappingTypesDetectedCallOutDescription"
|
||||||
|
defaultMessage="The mappings for this template uses types, which have been removed. {docsLink}"
|
||||||
|
values={{
|
||||||
|
docsLink: (
|
||||||
|
<EuiLink href={documentationService.getAlternativeToMappingTypesLink()} target="_blank">
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.idxMgmt.mappingsEditor.mappingTypesDetectedCallOutDocumentationLink',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Consider these alternatives to mapping types.',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</EuiCallOut>
|
||||||
|
);
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* 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 { extractMappingsDefinition } from './extract_mappings_definition';
|
||||||
|
|
||||||
|
describe('extractMappingsDefinition', () => {
|
||||||
|
test('should detect that the mappings has multiple types and return null', () => {
|
||||||
|
const mappings = {
|
||||||
|
type1: {
|
||||||
|
properties: {
|
||||||
|
name1: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type2: {
|
||||||
|
properties: {
|
||||||
|
name2: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(extractMappingsDefinition(mappings)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect that the mappings has multiple types even when one of the type has not defined any "properties"', () => {
|
||||||
|
const mappings = {
|
||||||
|
type1: {
|
||||||
|
_source: {
|
||||||
|
excludes: [],
|
||||||
|
includes: [],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
_routing: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type2: {
|
||||||
|
properties: {
|
||||||
|
name2: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(extractMappingsDefinition(mappings)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect that one of the mapping type is invalid and filter it out', () => {
|
||||||
|
const mappings = {
|
||||||
|
type1: {
|
||||||
|
invalidSetting: {
|
||||||
|
excludes: [],
|
||||||
|
includes: [],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
_routing: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type2: {
|
||||||
|
properties: {
|
||||||
|
name2: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(extractMappingsDefinition(mappings)).toBe(mappings.type2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect that the mappings has one type and return its mapping definition', () => {
|
||||||
|
const mappings = {
|
||||||
|
myType: {
|
||||||
|
_source: {
|
||||||
|
excludes: [],
|
||||||
|
includes: [],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
_meta: {},
|
||||||
|
_routing: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
dynamic: true,
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(extractMappingsDefinition(mappings)).toBe(mappings.myType);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect that the mappings has one type at root level', () => {
|
||||||
|
const mappings = {
|
||||||
|
_source: {
|
||||||
|
excludes: [],
|
||||||
|
includes: [],
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
_meta: {},
|
||||||
|
_routing: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
dynamic: true,
|
||||||
|
numeric_detection: false,
|
||||||
|
date_detection: true,
|
||||||
|
dynamic_date_formats: ['strict_date_optional_time'],
|
||||||
|
dynamic_templates: [],
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(extractMappingsDefinition(mappings)).toBe(mappings);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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 { isPlainObject } from 'lodash';
|
||||||
|
|
||||||
|
import { GenericObject } from '../types';
|
||||||
|
import {
|
||||||
|
validateMappingsConfiguration,
|
||||||
|
mappingsConfigurationSchemaKeys,
|
||||||
|
} from './mappings_validator';
|
||||||
|
|
||||||
|
const ALLOWED_PARAMETERS = [...mappingsConfigurationSchemaKeys, 'dynamic_templates', 'properties'];
|
||||||
|
|
||||||
|
const isMappingDefinition = (obj: GenericObject): boolean => {
|
||||||
|
const areAllKeysValid = Object.keys(obj).every(key => ALLOWED_PARAMETERS.includes(key));
|
||||||
|
|
||||||
|
if (!areAllKeysValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { properties, dynamic_templates: dynamicTemplates, ...mappingsConfiguration } = obj;
|
||||||
|
|
||||||
|
const { errors } = validateMappingsConfiguration(mappingsConfiguration);
|
||||||
|
const isConfigurationValid = errors.length === 0;
|
||||||
|
const isPropertiesValid = properties === undefined || isPlainObject(properties);
|
||||||
|
const isDynamicTemplatesValid = dynamicTemplates === undefined || Array.isArray(dynamicTemplates);
|
||||||
|
|
||||||
|
// If the configuration, the properties and the dynamic templates are valid
|
||||||
|
// we can assume that the mapping is declared at root level (no types)
|
||||||
|
return isConfigurationValid && isPropertiesValid && isDynamicTemplatesValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5.x index templates can be created with multiple types.
|
||||||
|
* e.g.
|
||||||
|
```
|
||||||
|
const mappings = {
|
||||||
|
type1: {
|
||||||
|
properties: {
|
||||||
|
name1: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type2: {
|
||||||
|
properties: {
|
||||||
|
name2: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
* A mappings can also be declared under an explicit "_doc" property.
|
||||||
|
```
|
||||||
|
const mappings = {
|
||||||
|
_doc: {
|
||||||
|
_source: {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
name1: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
* This helpers parse the mappings provided an removes any possible mapping "type" declared
|
||||||
|
*
|
||||||
|
* @param mappings The mappings object to validate
|
||||||
|
*/
|
||||||
|
export const extractMappingsDefinition = (mappings: GenericObject = {}): GenericObject | null => {
|
||||||
|
if (isMappingDefinition(mappings)) {
|
||||||
|
// No need to go any further
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point there must be one or more type mappings
|
||||||
|
const typedMappings = Object.values(mappings).reduce((acc: GenericObject[], value) => {
|
||||||
|
if (isMappingDefinition(value)) {
|
||||||
|
acc.push(value as GenericObject);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// If there are no typed mappings found this means that one of the type must did not pass
|
||||||
|
// the "isMappingDefinition()" validation.
|
||||||
|
// In theory this should never happen but let's make sure the UI does not try to load an invalid mapping
|
||||||
|
if (typedMappings.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's only one mapping type then we can consume it as if the type doesn't exist.
|
||||||
|
if (typedMappings.length === 1) {
|
||||||
|
return typedMappings[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's more than one mapping type, then the mappings object isn't usable.
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -13,3 +13,5 @@ export * from './validators';
|
||||||
export * from './mappings_validator';
|
export * from './mappings_validator';
|
||||||
|
|
||||||
export * from './search_fields';
|
export * from './search_fields';
|
||||||
|
|
||||||
|
export * from './extract_mappings_definition';
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
import { isPlainObject } from 'lodash';
|
||||||
|
|
||||||
import { validateMappings, validateProperties, isObject } from './mappings_validator';
|
import { validateMappings, validateProperties } from './mappings_validator';
|
||||||
|
|
||||||
describe('Mappings configuration validator', () => {
|
describe('Mappings configuration validator', () => {
|
||||||
it('should convert non object to empty object', () => {
|
it('should convert non object to empty object', () => {
|
||||||
|
@ -12,7 +13,7 @@ describe('Mappings configuration validator', () => {
|
||||||
|
|
||||||
tests.forEach(testValue => {
|
tests.forEach(testValue => {
|
||||||
const { value, errors } = validateMappings(testValue as any);
|
const { value, errors } = validateMappings(testValue as any);
|
||||||
expect(isObject(value)).toBe(true);
|
expect(isPlainObject(value)).toBe(true);
|
||||||
expect(errors).toBe(undefined);
|
expect(errors).toBe(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -76,7 +77,7 @@ describe('Properties validator', () => {
|
||||||
|
|
||||||
tests.forEach(testValue => {
|
tests.forEach(testValue => {
|
||||||
const { value, errors } = validateProperties(testValue as any);
|
const { value, errors } = validateProperties(testValue as any);
|
||||||
expect(isObject(value)).toBe(true);
|
expect(isPlainObject(value)).toBe(true);
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,15 +3,16 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import { pick } from 'lodash';
|
import { pick, isPlainObject } from 'lodash';
|
||||||
import * as t from 'io-ts';
|
import * as t from 'io-ts';
|
||||||
import { ordString } from 'fp-ts/lib/Ord';
|
import { ordString } from 'fp-ts/lib/Ord';
|
||||||
import { toArray } from 'fp-ts/lib/Set';
|
import { toArray } from 'fp-ts/lib/Set';
|
||||||
import { isLeft, isRight } from 'fp-ts/lib/Either';
|
import { isLeft, isRight } from 'fp-ts/lib/Either';
|
||||||
|
|
||||||
import { errorReporter } from './error_reporter';
|
import { errorReporter } from './error_reporter';
|
||||||
import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants';
|
import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants';
|
||||||
import { FieldMeta } from '../types';
|
import { FieldMeta } from '../types';
|
||||||
import { getFieldMeta } from '../lib';
|
import { getFieldMeta } from './utils';
|
||||||
|
|
||||||
const ALLOWED_FIELD_PROPERTIES = [
|
const ALLOWED_FIELD_PROPERTIES = [
|
||||||
...Object.keys(PARAMETERS_DEFINITION),
|
...Object.keys(PARAMETERS_DEFINITION),
|
||||||
|
@ -49,8 +50,6 @@ interface GenericObject {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isObject = (obj: any) => obj != null && obj.constructor.name === 'Object';
|
|
||||||
|
|
||||||
const validateFieldType = (type: any): boolean => {
|
const validateFieldType = (type: any): boolean => {
|
||||||
if (typeof type !== 'string') {
|
if (typeof type !== 'string') {
|
||||||
return false;
|
return false;
|
||||||
|
@ -72,7 +71,7 @@ const validateParameter = (parameter: string, value: any): boolean => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parameter === 'properties' || parameter === 'fields') {
|
if (parameter === 'properties' || parameter === 'fields') {
|
||||||
return isObject(value);
|
return isPlainObject(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameterSchema = (PARAMETERS_DEFINITION as any)[parameter]!.schema;
|
const parameterSchema = (PARAMETERS_DEFINITION as any)[parameter]!.schema;
|
||||||
|
@ -100,7 +99,7 @@ const stripUnknownOrInvalidParameter = (field: GenericObject): FieldValidatorRes
|
||||||
|
|
||||||
const parseField = (field: any): FieldValidatorResponse & { meta?: FieldMeta } => {
|
const parseField = (field: any): FieldValidatorResponse & { meta?: FieldMeta } => {
|
||||||
// Sanitize the input to make sure we are working with an object
|
// Sanitize the input to make sure we are working with an object
|
||||||
if (!isObject(field)) {
|
if (!isPlainObject(field)) {
|
||||||
return { parametersRemoved: [] };
|
return { parametersRemoved: [] };
|
||||||
}
|
}
|
||||||
// Make sure the field "type" is valid
|
// Make sure the field "type" is valid
|
||||||
|
@ -186,7 +185,7 @@ const parseFields = (
|
||||||
*/
|
*/
|
||||||
export const validateProperties = (properties = {}): PropertiesValidatorResponse => {
|
export const validateProperties = (properties = {}): PropertiesValidatorResponse => {
|
||||||
// Sanitize the input to make sure we are working with an object
|
// Sanitize the input to make sure we are working with an object
|
||||||
if (!isObject(properties)) {
|
if (!isPlainObject(properties)) {
|
||||||
return { value: {}, errors: [] };
|
return { value: {}, errors: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,9 +212,9 @@ export const mappingsConfigurationSchema = t.partial({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mappingsConfigurationSchemaKeys = Object.keys(mappingsConfigurationSchema.props);
|
export const mappingsConfigurationSchemaKeys = Object.keys(mappingsConfigurationSchema.props);
|
||||||
|
|
||||||
const validateMappingsConfiguration = (
|
export const validateMappingsConfiguration = (
|
||||||
mappingsConfiguration: any
|
mappingsConfiguration: any
|
||||||
): { value: any; errors: MappingsValidationError[] } => {
|
): { value: any; errors: MappingsValidationError[] } => {
|
||||||
// Set to keep track of invalid configuration parameters.
|
// Set to keep track of invalid configuration parameters.
|
||||||
|
@ -249,7 +248,7 @@ const validateMappingsConfiguration = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateMappings = (mappings: any = {}): MappingsValidatorResponse => {
|
export const validateMappings = (mappings: any = {}): MappingsValidatorResponse => {
|
||||||
if (!isObject(mappings)) {
|
if (!isPlainObject(mappings)) {
|
||||||
return { value: {} };
|
return { value: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,20 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState, useEffect } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
|
import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
|
||||||
|
|
||||||
import { ConfigurationForm, DocumentFields, TemplatesForm } from './components';
|
import {
|
||||||
|
ConfigurationForm,
|
||||||
|
DocumentFields,
|
||||||
|
TemplatesForm,
|
||||||
|
MultipleMappingsWarning,
|
||||||
|
} from './components';
|
||||||
import { IndexSettings } from './types';
|
import { IndexSettings } from './types';
|
||||||
|
import { extractMappingsDefinition } from './lib';
|
||||||
import { State } from './reducer';
|
import { State } from './reducer';
|
||||||
import { MappingsState, Props as MappingsStateProps } from './mappings_state';
|
import { MappingsState, Props as MappingsStateProps, Types } from './mappings_state';
|
||||||
import { IndexSettingsProvider } from './index_settings_context';
|
import { IndexSettingsProvider } from './index_settings_context';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -25,7 +31,13 @@ type TabName = 'fields' | 'advanced' | 'templates';
|
||||||
export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => {
|
export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSettings }: Props) => {
|
||||||
const [selectedTab, selectTab] = useState<TabName>('fields');
|
const [selectedTab, selectTab] = useState<TabName>('fields');
|
||||||
|
|
||||||
const parsedDefaultValue = useMemo(() => {
|
const { parsedDefaultValue, multipleMappingsDeclared } = useMemo(() => {
|
||||||
|
const mappingsDefinition = extractMappingsDefinition(defaultValue);
|
||||||
|
|
||||||
|
if (mappingsDefinition === null) {
|
||||||
|
return { multipleMappingsDeclared: true };
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
_source = {},
|
_source = {},
|
||||||
_meta = {},
|
_meta = {},
|
||||||
|
@ -36,9 +48,9 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
|
||||||
dynamic_date_formats,
|
dynamic_date_formats,
|
||||||
properties = {},
|
properties = {},
|
||||||
dynamic_templates,
|
dynamic_templates,
|
||||||
} = defaultValue ?? {};
|
} = mappingsDefinition;
|
||||||
|
|
||||||
return {
|
const parsed = {
|
||||||
configuration: {
|
configuration: {
|
||||||
_source,
|
_source,
|
||||||
_meta,
|
_meta,
|
||||||
|
@ -53,8 +65,21 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
|
||||||
dynamic_templates,
|
dynamic_templates,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return { parsedDefaultValue: parsed, multipleMappingsDeclared: false };
|
||||||
}, [defaultValue]);
|
}, [defaultValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (multipleMappingsDeclared) {
|
||||||
|
// We set the data getter here as the user won't be able to make any changes
|
||||||
|
onUpdate({
|
||||||
|
getData: () => defaultValue! as Types['Mappings'],
|
||||||
|
validate: () => Promise.resolve(true),
|
||||||
|
isValid: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [multipleMappingsDeclared]);
|
||||||
|
|
||||||
const changeTab = async (tab: TabName, state: State) => {
|
const changeTab = async (tab: TabName, state: State) => {
|
||||||
if (selectedTab === 'advanced') {
|
if (selectedTab === 'advanced') {
|
||||||
// When we navigate away we need to submit the form to validate if there are any errors.
|
// When we navigate away we need to submit the form to validate if there are any errors.
|
||||||
|
@ -63,7 +88,6 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
|
||||||
if (!isConfigurationFormValid) {
|
if (!isConfigurationFormValid) {
|
||||||
/**
|
/**
|
||||||
* Don't navigate away from the tab if there are errors in the form.
|
* Don't navigate away from the tab if there are errors in the form.
|
||||||
* For now there is no need to display a CallOut as the form can never be invalid.
|
|
||||||
*/
|
*/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -79,51 +103,57 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IndexSettingsProvider indexSettings={indexSettings}>
|
<div data-test-subj="mappingsEditor">
|
||||||
<MappingsState onUpdate={onUpdate} defaultValue={parsedDefaultValue}>
|
{multipleMappingsDeclared ? (
|
||||||
{({ state }) => {
|
<MultipleMappingsWarning />
|
||||||
const tabToContentMap = {
|
) : (
|
||||||
fields: <DocumentFields />,
|
<IndexSettingsProvider indexSettings={indexSettings}>
|
||||||
templates: <TemplatesForm defaultValue={state.templates.defaultValue} />,
|
<MappingsState onUpdate={onUpdate} defaultValue={parsedDefaultValue!}>
|
||||||
advanced: <ConfigurationForm defaultValue={state.configuration.defaultValue} />,
|
{({ state }) => {
|
||||||
};
|
const tabToContentMap = {
|
||||||
|
fields: <DocumentFields />,
|
||||||
|
templates: <TemplatesForm defaultValue={state.templates.defaultValue} />,
|
||||||
|
advanced: <ConfigurationForm defaultValue={state.configuration.defaultValue} />,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mappingsEditor">
|
<div className="mappingsEditor">
|
||||||
<EuiTabs>
|
<EuiTabs>
|
||||||
<EuiTab
|
<EuiTab
|
||||||
onClick={() => changeTab('fields', state)}
|
onClick={() => changeTab('fields', state)}
|
||||||
isSelected={selectedTab === 'fields'}
|
isSelected={selectedTab === 'fields'}
|
||||||
>
|
>
|
||||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
|
{i18n.translate('xpack.idxMgmt.mappingsEditor.fieldsTabLabel', {
|
||||||
defaultMessage: 'Mapped fields',
|
defaultMessage: 'Mapped fields',
|
||||||
})}
|
})}
|
||||||
</EuiTab>
|
</EuiTab>
|
||||||
<EuiTab
|
<EuiTab
|
||||||
onClick={() => changeTab('templates', state)}
|
onClick={() => changeTab('templates', state)}
|
||||||
isSelected={selectedTab === 'templates'}
|
isSelected={selectedTab === 'templates'}
|
||||||
>
|
>
|
||||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
|
{i18n.translate('xpack.idxMgmt.mappingsEditor.templatesTabLabel', {
|
||||||
defaultMessage: 'Dynamic templates',
|
defaultMessage: 'Dynamic templates',
|
||||||
})}
|
})}
|
||||||
</EuiTab>
|
</EuiTab>
|
||||||
<EuiTab
|
<EuiTab
|
||||||
onClick={() => changeTab('advanced', state)}
|
onClick={() => changeTab('advanced', state)}
|
||||||
isSelected={selectedTab === 'advanced'}
|
isSelected={selectedTab === 'advanced'}
|
||||||
>
|
>
|
||||||
{i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
|
{i18n.translate('xpack.idxMgmt.mappingsEditor.advancedTabLabel', {
|
||||||
defaultMessage: 'Advanced options',
|
defaultMessage: 'Advanced options',
|
||||||
})}
|
})}
|
||||||
</EuiTab>
|
</EuiTab>
|
||||||
</EuiTabs>
|
</EuiTabs>
|
||||||
|
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
{tabToContentMap[selectedTab]}
|
{tabToContentMap[selectedTab]}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</MappingsState>
|
</MappingsState>
|
||||||
</IndexSettingsProvider>
|
</IndexSettingsProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -287,3 +287,7 @@ export interface SearchMetadata {
|
||||||
*/
|
*/
|
||||||
stringMatch: string | null;
|
stringMatch: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GenericObject {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
|
@ -181,6 +181,10 @@ class DocumentationService {
|
||||||
return `${this.esDocsBase}/index-options.html`;
|
return `${this.esDocsBase}/index-options.html`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAlternativeToMappingTypesLink() {
|
||||||
|
return `${this.esDocsBase}/removal-of-types.html#_alternatives_to_mapping_types`;
|
||||||
|
}
|
||||||
|
|
||||||
public getJoinMultiLevelsPerformanceLink() {
|
public getJoinMultiLevelsPerformanceLink() {
|
||||||
return `${this.esDocsBase}/parent-join.html#_parent_join_and_performance`;
|
return `${this.esDocsBase}/parent-join.html#_parent_join_and_performance`;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue