Skip to content

Commit

Permalink
[Mappings editor] Accommodate legacy index templates (#55388)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored Jan 23, 2020
1 parent 071db79 commit 2cc9ed6
Show file tree
Hide file tree
Showing 14 changed files with 471 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -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 },
};
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const DocumentFields = React.memo(() => {
const searchTerm = search.term.trim();

return (
<>
<div data-test-subj="documentFields">
<DocumentFieldsHeader searchValue={search.term} onSearchChange={onSearchChange} />
<EuiSpacer size="m" />
{searchTerm !== '' ? (
Expand All @@ -57,6 +57,6 @@ export const DocumentFields = React.memo(() => {
editor
)}
{renderEditField()}
</>
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './configuration_form';
export * from './document_fields';

export * from './templates_form';

export * from './multiple_mappings_warning';
Original file line number Diff line number Diff line change
@@ -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>
);
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export * from './validators';
export * from './mappings_validator';

export * from './search_fields';

export * from './extract_mappings_definition';
Loading

0 comments on commit 2cc9ed6

Please sign in to comment.