From 218cbed2965f9eaa9bf43a6db4eefc7fa6c929eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Wed, 29 Jan 2020 14:57:40 +0530 Subject: [PATCH 01/10] Add mappings editor lib, constants and types --- .../constants/data_types_definition.tsx | 113 ++++ .../constants/default_values.ts | 14 + .../constants/field_options.tsx | 255 +++++++++ .../constants/field_options_i18n.ts | 495 ++++++++++++++++++ .../mappings_editor/constants/index.ts | 15 + .../constants/mappings_editor.ts | 21 + .../constants/parameters_definition.tsx | 73 +++ .../components/mappings_editor/index.ts | 7 + .../mappings_editor/lib/error_reporter.ts | 42 ++ .../lib/extract_mappings_definition.test.ts | 161 ++++++ .../lib/extract_mappings_definition.ts | 117 +++++ .../components/mappings_editor/lib/index.ts | 11 + .../lib/mappings_validator.test.ts | 360 +++++++++++++ .../mappings_editor/lib/mappings_validator.ts | 311 +++++++++++ .../mappings_editor/lib/utils.test.ts | 65 +++ .../components/mappings_editor/lib/utils.ts | 487 +++++++++++++++++ .../components/mappings_editor/types.ts | 293 +++++++++++ 17 files changed, 2840 insertions(+) create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/index.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/error_reporter.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts create mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx new file mode 100644 index 0000000000000..965057505cd15 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx @@ -0,0 +1,113 @@ +/* + * 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 { MainType, SubType, DataType, DataTypeDefinition } from '../types'; + +export const TYPE_DEFINITION: { [key in DataType]: boolean } = { + text: true, + keyword: true, + numeric: true, + byte: true, + double: true, + integer: true, + long: true, + float: true, + half_float: true, + scaled_float: true, + short: true, + date: true, + date_nanos: true, + binary: true, + ip: true, + boolean: true, + range: true, + object: true, + nested: true, + rank_feature: true, + rank_features: true, + dense_vector: true, + date_range: true, + double_range: true, + float_range: true, + integer_range: true, + long_range: true, + ip_range: true, + geo_point: true, + geo_shape: true, + completion: true, + token_count: true, + percolator: true, + join: true, + alias: true, + search_as_you_type: true, + flattened: true, + shape: true, +}; + +export const MAIN_TYPES: MainType[] = [ + 'alias', + 'binary', + 'boolean', + 'completion', + 'date', + 'date_nanos', + 'dense_vector', + 'flattened', + 'geo_point', + 'geo_shape', + 'ip', + 'join', + 'keyword', + 'nested', + 'numeric', + 'object', + 'percolator', + 'range', + 'rank_feature', + 'rank_features', + 'search_as_you_type', + 'shape', + 'text', + 'token_count', +]; + +export const MAIN_DATA_TYPE_DEFINITION: { + [key in MainType]: DataTypeDefinition; +} = MAIN_TYPES.reduce( + (acc, type) => ({ + ...acc, + [type]: TYPE_DEFINITION[type], + }), + {} as { [key in MainType]: DataTypeDefinition } +); + +/** + * Return a map of subType -> mainType + * + * @example + * + * { + * long: 'numeric', + * integer: 'numeric', + * short: 'numeric', + * } + */ +export const SUB_TYPE_MAP_TO_MAIN = Object.entries(MAIN_DATA_TYPE_DEFINITION).reduce( + (acc, [type, definition]) => { + if ({}.hasOwnProperty.call(definition, 'subTypes')) { + definition.subTypes!.types.forEach(subType => { + acc[subType] = type; + }); + } + return acc; + }, + {} as Record +); + +// Single source of truth of all the possible data types. +export const ALL_DATA_TYPES = [ + ...Object.keys(MAIN_DATA_TYPE_DEFINITION), + ...Object.keys(SUB_TYPE_MAP_TO_MAIN), +]; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts new file mode 100644 index 0000000000000..96623b855dd3a --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * When we want to set a parameter value to the index "default" in a Select option + * we will use this constant to define it. We will then strip this placeholder value + * and let Elasticsearch handle it. + */ +export const INDEX_DEFAULT = 'index_default'; + +export const STANDARD = 'standard'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx new file mode 100644 index 0000000000000..710e637de8b08 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx @@ -0,0 +1,255 @@ +/* + * 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 { EuiText } from '@elastic/eui'; + +import { DataType, ParameterName, SelectOption, SuperSelectOption, ComboBoxOption } from '../types'; +import { FIELD_OPTIONS_TEXTS, LANGUAGE_OPTIONS_TEXT, FieldOption } from './field_options_i18n'; +import { INDEX_DEFAULT, STANDARD } from './default_values'; +import { MAIN_DATA_TYPE_DEFINITION } from './data_types_definition'; + +export const TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL: DataType[] = ['join']; + +export const TYPE_NOT_ALLOWED_MULTIFIELD: DataType[] = [ + ...TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL, + 'object', + 'nested', + 'alias', +]; + +export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map( + ([dataType, { label }]) => ({ + value: dataType, + label, + }) +) as ComboBoxOption[]; + +interface SuperSelectOptionConfig { + inputDisplay: string; + dropdownDisplay: JSX.Element; +} + +export const getSuperSelectOption = ( + title: string, + description: string +): SuperSelectOptionConfig => ({ + inputDisplay: title, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), +}); + +const getOptionTexts = (option: FieldOption): SuperSelectOptionConfig => + getSuperSelectOption(FIELD_OPTIONS_TEXTS[option].title, FIELD_OPTIONS_TEXTS[option].description); + +type ParametersOptions = ParameterName | 'languageAnalyzer'; + +export const PARAMETERS_OPTIONS: { + [key in ParametersOptions]?: SelectOption[] | SuperSelectOption[]; +} = { + index_options: [ + { + value: 'docs', + ...getOptionTexts('indexOptions.docs'), + }, + { + value: 'freqs', + ...getOptionTexts('indexOptions.freqs'), + }, + { + value: 'positions', + ...getOptionTexts('indexOptions.positions'), + }, + { + value: 'offsets', + ...getOptionTexts('indexOptions.offsets'), + }, + ] as SuperSelectOption[], + index_options_flattened: [ + { + value: 'docs', + ...getOptionTexts('indexOptions.docs'), + }, + { + value: 'freqs', + ...getOptionTexts('indexOptions.freqs'), + }, + ] as SuperSelectOption[], + index_options_keyword: [ + { + value: 'docs', + ...getOptionTexts('indexOptions.docs'), + }, + { + value: 'freqs', + ...getOptionTexts('indexOptions.freqs'), + }, + ] as SuperSelectOption[], + analyzer: [ + { + value: INDEX_DEFAULT, + ...getOptionTexts('analyzer.indexDefault'), + }, + { + value: STANDARD, + ...getOptionTexts('analyzer.standard'), + }, + { + value: 'simple', + ...getOptionTexts('analyzer.simple'), + }, + { + value: 'whitespace', + ...getOptionTexts('analyzer.whitespace'), + }, + { + value: 'stop', + ...getOptionTexts('analyzer.stop'), + }, + { + value: 'keyword', + ...getOptionTexts('analyzer.keyword'), + }, + { + value: 'pattern', + ...getOptionTexts('analyzer.pattern'), + }, + { + value: 'fingerprint', + ...getOptionTexts('analyzer.fingerprint'), + }, + { + value: 'language', + ...getOptionTexts('analyzer.language'), + }, + ] as SuperSelectOption[], + languageAnalyzer: Object.entries(LANGUAGE_OPTIONS_TEXT).map(([value, text]) => ({ + value, + text, + })), + similarity: [ + { + value: 'BM25', + ...getOptionTexts('similarity.bm25'), + }, + { + value: 'boolean', + ...getOptionTexts('similarity.boolean'), + }, + ] as SuperSelectOption[], + term_vector: [ + { + value: 'no', + ...getOptionTexts('termVector.no'), + }, + { + value: 'yes', + ...getOptionTexts('termVector.yes'), + }, + { + value: 'with_positions', + ...getOptionTexts('termVector.withPositions'), + }, + { + value: 'with_offsets', + ...getOptionTexts('termVector.withOffsets'), + }, + { + value: 'with_positions_offsets', + ...getOptionTexts('termVector.withPositionsOffsets'), + }, + { + value: 'with_positions_payloads', + ...getOptionTexts('termVector.withPositionsPayloads'), + }, + { + value: 'with_positions_offsets_payloads', + ...getOptionTexts('termVector.withPositionsOffsetsPayloads'), + }, + ] as SuperSelectOption[], + orientation: [ + { + value: 'ccw', + ...getOptionTexts('orientation.counterclockwise'), + }, + { + value: 'cw', + ...getOptionTexts('orientation.clockwise'), + }, + ] as SuperSelectOption[], +}; + +const DATE_FORMATS = [ + { label: 'epoch_millis' }, + { label: 'epoch_second' }, + { label: 'date_optional_time', strict: true }, + { label: 'basic_date' }, + { label: 'basic_date_time' }, + { label: 'basic_date_time_no_millis' }, + { label: 'basic_ordinal_date' }, + { label: 'basic_ordinal_date_time' }, + { label: 'basic_ordinal_date_time_no_millis' }, + { label: 'basic_time' }, + { label: 'basic_time_no_millis' }, + { label: 'basic_t_time' }, + { label: 'basic_t_time_no_millis' }, + { label: 'basic_week_date', strict: true }, + { label: 'basic_week_date_time', strict: true }, + { + label: 'basic_week_date_time_no_millis', + strict: true, + }, + { label: 'date', strict: true }, + { label: 'date_hour', strict: true }, + { label: 'date_hour_minute', strict: true }, + { label: 'date_hour_minute_second', strict: true }, + { + label: 'date_hour_minute_second_fraction', + strict: true, + }, + { + label: 'date_hour_minute_second_millis', + strict: true, + }, + { label: 'date_time', strict: true }, + { label: 'date_time_no_millis', strict: true }, + { label: 'hour', strict: true }, + { label: 'hour_minute ', strict: true }, + { label: 'hour_minute_second', strict: true }, + { label: 'hour_minute_second_fraction', strict: true }, + { label: 'hour_minute_second_millis', strict: true }, + { label: 'ordinal_date', strict: true }, + { label: 'ordinal_date_time', strict: true }, + { label: 'ordinal_date_time_no_millis', strict: true }, + { label: 'time', strict: true }, + { label: 'time_no_millis', strict: true }, + { label: 't_time', strict: true }, + { label: 't_time_no_millis', strict: true }, + { label: 'week_date', strict: true }, + { label: 'week_date_time', strict: true }, + { label: 'week_date_time_no_millis', strict: true }, + { label: 'weekyear', strict: true }, + { label: 'weekyear_week', strict: true }, + { label: 'weekyear_week_day', strict: true }, + { label: 'year', strict: true }, + { label: 'year_month', strict: true }, + { label: 'year_month_day', strict: true }, +]; + +const STRICT_DATE_FORMAT_OPTIONS = DATE_FORMATS.filter(format => format.strict).map( + ({ label }) => ({ + label: `strict_${label}`, + }) +); + +const DATE_FORMAT_OPTIONS = DATE_FORMATS.map(({ label }) => ({ label })); + +export const ALL_DATE_FORMAT_OPTIONS = [...DATE_FORMAT_OPTIONS, ...STRICT_DATE_FORMAT_OPTIONS]; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts new file mode 100644 index 0000000000000..15079d520f2ad --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts @@ -0,0 +1,495 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +interface Optioni18n { + title: string; + description: string; +} + +type IndexOptions = + | 'indexOptions.docs' + | 'indexOptions.freqs' + | 'indexOptions.positions' + | 'indexOptions.offsets'; + +type AnalyzerOptions = + | 'analyzer.indexDefault' + | 'analyzer.standard' + | 'analyzer.simple' + | 'analyzer.whitespace' + | 'analyzer.stop' + | 'analyzer.keyword' + | 'analyzer.pattern' + | 'analyzer.fingerprint' + | 'analyzer.language'; + +type SimilarityOptions = 'similarity.bm25' | 'similarity.boolean'; + +type TermVectorOptions = + | 'termVector.no' + | 'termVector.yes' + | 'termVector.withPositions' + | 'termVector.withOffsets' + | 'termVector.withPositionsOffsets' + | 'termVector.withPositionsPayloads' + | 'termVector.withPositionsOffsetsPayloads'; + +type OrientationOptions = 'orientation.counterclockwise' | 'orientation.clockwise'; + +type LanguageAnalyzerOption = + | 'arabic' + | 'armenian' + | 'basque' + | 'bengali' + | 'brazilian' + | 'bulgarian' + | 'catalan' + | 'cjk' + | 'czech' + | 'danish' + | 'dutch' + | 'english' + | 'finnish' + | 'french' + | 'galician' + | 'german' + | 'greek' + | 'hindi' + | 'hungarian' + | 'indonesian' + | 'irish' + | 'italian' + | 'latvian' + | 'lithuanian' + | 'norwegian' + | 'persian' + | 'portuguese' + | 'romanian' + | 'russian' + | 'sorani' + | 'spanish' + | 'swedish' + | 'turkish' + | 'thai'; + +export type FieldOption = + | IndexOptions + | AnalyzerOptions + | SimilarityOptions + | TermVectorOptions + | OrientationOptions; + +export const FIELD_OPTIONS_TEXTS: { [key in FieldOption]: Optioni18n } = { + 'indexOptions.docs': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.docNumberTitle', { + defaultMessage: 'Doc number', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.docNumberDescription', + { + defaultMessage: + 'Index the doc number only. Used to verify the existence of a term in a field.', + } + ), + }, + 'indexOptions.freqs': { + title: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.termFrequencyTitle', + { + defaultMessage: 'Term frequencies', + } + ), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.termFrequencyDescription', + { + defaultMessage: + 'Index the doc number and term frequencies. Repeated terms score higher than single terms.', + } + ), + }, + 'indexOptions.positions': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.positionsTitle', { + defaultMessage: 'Positions', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.positionsDescription', + { + defaultMessage: + 'Index the doc number, term frequencies, positions, and start and end character offsets. Offsets map the term back to the original string.', + } + ), + }, + 'indexOptions.offsets': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.offsetsTitle', { + defaultMessage: 'Offsets', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.offsetsDescription', + { + defaultMessage: + 'Doc number, term frequencies, positions, and start and end character offsets (which map the term back to the original string) are indexed.', + } + ), + }, + 'analyzer.indexDefault': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.indexDefaultTitle', { + defaultMessage: 'Index default', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.indexDefaultDescription', + { + defaultMessage: 'Use the analyzer defined for the index.', + } + ), + }, + 'analyzer.standard': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.standardTitle', { + defaultMessage: 'Standard', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.standardDescription', + { + defaultMessage: + 'The standard analyzer divides text into terms on word boundaries, as defined by the Unicode Text Segmentation algorithm.', + } + ), + }, + 'analyzer.simple': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.simpleTitle', { + defaultMessage: 'Simple', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.simpleDescription', + { + defaultMessage: + 'The simple analyzer divides text into terms whenever it encounters a character which is not a letter. ', + } + ), + }, + 'analyzer.whitespace': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.whitespaceTitle', { + defaultMessage: 'Whitespace', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.whitespaceDescription', + { + defaultMessage: + 'The whitespace analyzer divides text into terms whenever it encounters any whitespace character.', + } + ), + }, + 'analyzer.stop': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.stopTitle', { + defaultMessage: 'Stop', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.stopDescription', + { + defaultMessage: + 'The stop analyzer is like the simple analyzer, but also supports removal of stop words.', + } + ), + }, + 'analyzer.keyword': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.keywordTitle', { + defaultMessage: 'Keyword', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.keywordDescription', + { + defaultMessage: + 'The keyword analyzer is a “noop” analyzer that accepts whatever text it is given and outputs the exact same text as a single term.', + } + ), + }, + 'analyzer.pattern': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.patternTitle', { + defaultMessage: 'Pattern', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.patternDescription', + { + defaultMessage: + 'The pattern analyzer uses a regular expression to split the text into terms. It supports lower-casing and stop words.', + } + ), + }, + 'analyzer.fingerprint': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.fingerprintTitle', { + defaultMessage: 'Fingerprint', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.fingerprintDescription', + { + defaultMessage: + 'The fingerprint analyzer is a specialist analyzer which creates a fingerprint which can be used for duplicate detection.', + } + ), + }, + 'analyzer.language': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.languageTitle', { + defaultMessage: 'Language', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.languageDescription', + { + defaultMessage: + 'Elasticsearch provides many language-specific analyzers like english or french.', + } + ), + }, + 'similarity.bm25': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.similarity.bm25Title', { + defaultMessage: 'Okapi BM25', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.similarity.bm25Description', + { + defaultMessage: 'The default algorithm used in Elasticsearch and Lucene.', + } + ), + }, + 'similarity.boolean': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.similarity.booleanTitle', { + defaultMessage: 'Boolean', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.similarity.booleanDescription', + { + defaultMessage: + 'A boolean similarity to use when full text-ranking is not needed. The score is based on whether the query terms match.', + } + ), + }, + 'termVector.no': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.noTitle', { + defaultMessage: 'No', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.noDescription', + { + defaultMessage: 'No term vectors are stored.', + } + ), + }, + 'termVector.yes': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.yesTitle', { + defaultMessage: 'Yes', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.yesDescription', + { + defaultMessage: 'Just the terms in the field are stored.', + } + ), + }, + 'termVector.withPositions': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsTitle', { + defaultMessage: 'With positions', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsDescription', + { + defaultMessage: 'Terms and positions are stored.', + } + ), + }, + 'termVector.withOffsets': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.withOffsetsTitle', { + defaultMessage: 'With offsets', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withOffsetsDescription', + { + defaultMessage: 'Terms and character offsets are stored.', + } + ), + }, + 'termVector.withPositionsOffsets': { + title: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsTitle', + { + defaultMessage: 'With positions and offsets', + } + ), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsDescription', + { + defaultMessage: 'Terms, positions, and character offsets are stored.', + } + ), + }, + 'termVector.withPositionsPayloads': { + title: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsPayloadsTitle', + { + defaultMessage: 'With positions and payloads', + } + ), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsPayloadsDescription', + { + defaultMessage: 'Terms, positions, and payloads are stored.', + } + ), + }, + 'termVector.withPositionsOffsetsPayloads': { + title: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsPayloadsTitle', + { + defaultMessage: 'With positions, offsets, and payloads', + } + ), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsPayloadsDescription', + { + defaultMessage: 'Terms, positions, offsets and payloads are stored.', + } + ), + }, + 'orientation.counterclockwise': { + title: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.counterclockwiseTitle', + { + defaultMessage: 'Counterclockwise', + } + ), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.counterclockwiseDescription', + { + defaultMessage: + 'Defines outer polygon vertices in counterclockwise order and interior shape vertices in clockwise order. This is the Open Geospatial Consortium (OGC) and GeoJSON standard.', + } + ), + }, + 'orientation.clockwise': { + title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.orientation.clockwiseTitle', { + defaultMessage: 'Clockwise', + }), + description: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.clockwiseDescription', + { + defaultMessage: + 'Defines outer polygon vertices in clockwise order and interior shape vertices in counterclockwise order.', + } + ), + }, +}; + +export const LANGUAGE_OPTIONS_TEXT: { [key in LanguageAnalyzerOption]: string } = { + arabic: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.arabic', { + defaultMessage: 'Arabic', + }), + armenian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.armenian', { + defaultMessage: 'Armenian', + }), + basque: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.basque', { + defaultMessage: 'Basque', + }), + bengali: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.bengali', { + defaultMessage: 'Bengali', + }), + brazilian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.brazilian', { + defaultMessage: 'Brazilian', + }), + bulgarian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.bulgarian', { + defaultMessage: 'Bulgarian', + }), + catalan: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.catalan', { + defaultMessage: 'Catalan', + }), + cjk: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.cjk', { + defaultMessage: 'Cjk', + }), + czech: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.czech', { + defaultMessage: 'Czech', + }), + danish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.danish', { + defaultMessage: 'Danish', + }), + dutch: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.dutch', { + defaultMessage: 'Dutch', + }), + english: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.english', { + defaultMessage: 'English', + }), + finnish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.finnish', { + defaultMessage: 'Finnish', + }), + french: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.french', { + defaultMessage: 'French', + }), + galician: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.galician', { + defaultMessage: 'Galician', + }), + german: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.german', { + defaultMessage: 'German', + }), + greek: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.greek', { + defaultMessage: 'Greek', + }), + hindi: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.hindi', { + defaultMessage: 'Hindi', + }), + hungarian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.hungarian', { + defaultMessage: 'Hungarian', + }), + indonesian: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.indonesian', + { + defaultMessage: 'Indonesian', + } + ), + irish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.irish', { + defaultMessage: 'Irish', + }), + italian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.italian', { + defaultMessage: 'Italian', + }), + latvian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.latvian', { + defaultMessage: 'Latvian', + }), + lithuanian: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.lithuanian', + { + defaultMessage: 'Lithuanian', + } + ), + norwegian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.norwegian', { + defaultMessage: 'Norwegian', + }), + persian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.persian', { + defaultMessage: 'Persian', + }), + portuguese: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.portuguese', + { + defaultMessage: 'Portuguese', + } + ), + romanian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.romanian', { + defaultMessage: 'Romanian', + }), + russian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.russian', { + defaultMessage: 'Russian', + }), + sorani: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.sorani', { + defaultMessage: 'Sorani', + }), + spanish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.spanish', { + defaultMessage: 'Spanish', + }), + swedish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.swedish', { + defaultMessage: 'Swedish', + }), + thai: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.thai', { + defaultMessage: 'Thai', + }), + turkish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.turkish', { + defaultMessage: 'Turkish', + }), +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts new file mode 100644 index 0000000000000..8addf3d9c4284 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export * from './default_values'; + +export * from './field_options'; + +export * from './data_types_definition'; + +export * from './parameters_definition'; + +export * from './mappings_editor'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts new file mode 100644 index 0000000000000..1678e09512019 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * The max nested depth allowed for child fields. + * Above this thresold, the user has to use the JSON editor. + */ +export const MAX_DEPTH_DEFAULT_EDITOR = 4; + +/** + * 16px is the default $euiSize Sass variable. + * @link https://elastic.github.io/eui/#/guidelines/sass + */ +export const EUI_SIZE = 16; + +export const CHILD_FIELD_INDENT_SIZE = EUI_SIZE * 1.5; + +export const LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER = EUI_SIZE * 0.25; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx new file mode 100644 index 0000000000000..3d380336b5eb0 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx @@ -0,0 +1,73 @@ +/* + * 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 { ParameterName } from '../types'; + +/** + * Single source of truth for the parameters a user can change on _any_ field type. + * It is also the single source of truth for the parameters default values. + * + * As a consequence, if a parameter is *not* declared here, we won't be able to declare it in the Json editor. + */ +export const PARAMETERS_DEFINITION: { [key in ParameterName]: boolean } = { + name: true, + type: true, + store: true, + index: true, + doc_values: true, + doc_values_binary: true, + fielddata: true, + fielddata_frequency_filter: true, + fielddata_frequency_filter_percentage: true, + fielddata_frequency_filter_absolute: true, + coerce: true, + coerce_shape: true, + ignore_malformed: true, + null_value: true, + null_value_ip: true, + null_value_numeric: true, + null_value_boolean: true, + null_value_geo_point: true, + copy_to: true, + max_input_length: true, + locale: true, + orientation: true, + boost: true, + scaling_factor: true, + dynamic: true, + dynamic_toggle: true, + dynamic_strict: true, + enabled: true, + format: true, + analyzer: true, + search_analyzer: true, + search_quote_analyzer: true, + normalizer: true, + index_options: true, + index_options_keyword: true, + index_options_flattened: true, + eager_global_ordinals: true, + eager_global_ordinals_join: true, + index_phrases: true, + preserve_separators: true, + preserve_position_increments: true, + ignore_z_value: true, + points_only: true, + norms: true, + norms_keyword: true, + term_vector: true, + path: true, + position_increment_gap: true, + index_prefixes: true, + similarity: true, + split_queries_on_whitespace: true, + ignore_above: true, + enable_position_increments: true, + depth_limit: true, + dims: true, + relations: true, + max_shingle_size: true, +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/index.ts new file mode 100644 index 0000000000000..17555951fc027 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { doMappingsHaveType } from './lib'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/error_reporter.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/error_reporter.ts new file mode 100644 index 0000000000000..e9beee1071597 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/error_reporter.ts @@ -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 { ValidationError } from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { Reporter } from 'io-ts/lib/Reporter'; + +export type ReporterResult = Array<{ path: string[]; message: string }>; + +const failure = (validation: ValidationError[]): ReporterResult => { + return validation.map(e => { + const path: string[] = []; + let validationName = ''; + + e.context.forEach((ctx, idx) => { + if (ctx.key) { + path.push(ctx.key); + } + + if (idx === e.context.length - 1) { + validationName = ctx.type.name; + } + }); + const lastItemName = path[path.length - 1]; + return { + path, + message: + 'Invalid value ' + + JSON.stringify(e.value) + + ` supplied to ${lastItemName}(${validationName})`, + }; + }); +}; + +const empty: never[] = []; +const success = () => empty; + +export const errorReporter: Reporter = { + report: fold(failure, success), +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts new file mode 100644 index 0000000000000..cf399f55e660e --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts @@ -0,0 +1,161 @@ +/* + * 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)).toEqual({ + type: 'type2', + mappings: 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)).toEqual({ + type: 'myType', + mappings: mappings.myType, + }); + }); + + test('should detect that the mappings has one custom type whose name matches a mappings definition parameter', () => { + const mappings = { + dynamic: { + _source: { + excludes: [], + includes: [], + enabled: true, + }, + _meta: {}, + _routing: { + required: false, + }, + dynamic: true, + properties: { + title: { + type: 'keyword', + }, + }, + }, + }; + + expect(extractMappingsDefinition(mappings)).toEqual({ + type: 'dynamic', + mappings: mappings.dynamic, + }); + }); + + 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)).toEqual({ mappings }); + }); +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts new file mode 100644 index 0000000000000..9f2a3226b69e0 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts @@ -0,0 +1,117 @@ +/* + * 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, VALID_MAPPINGS_PARAMETERS } from './mappings_validator'; + +interface MappingsWithType { + type?: string; + mappings: GenericObject; +} + +const isMappingDefinition = (obj: GenericObject): boolean => { + const areAllKeysValid = Object.keys(obj).every(key => VALID_MAPPINGS_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; +}; + +const getMappingsDefinitionWithType = (mappings: GenericObject): MappingsWithType[] => { + 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.entries(mappings).reduce( + (acc: Array<{ type: string; mappings: GenericObject }>, [type, value]) => { + if (isMappingDefinition(value)) { + acc.push({ type, mappings: value as GenericObject }); + } + return acc; + }, + [] + ); + + return typedMappings; +}; + +export const doMappingsHaveType = (mappings: GenericObject = {}): boolean => + getMappingsDefinitionWithType(mappings).filter(({ type }) => type !== undefined).length > 0; + +/** + * 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 = {} +): MappingsWithType | null => { + const typedMappings = getMappingsDefinitionWithType(mappings); + + // 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; +}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts new file mode 100644 index 0000000000000..250795c56f322 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export * from './utils'; + +export * from './mappings_validator'; + +export * from './extract_mappings_definition'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts new file mode 100644 index 0000000000000..d67c267dda6ae --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts @@ -0,0 +1,360 @@ +/* + * 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 { validateMappings, validateProperties } from './mappings_validator'; + +describe('Mappings configuration validator', () => { + it('should convert non object to empty object', () => { + const tests = ['abc', 123, [], null, undefined]; + + tests.forEach(testValue => { + const { value, errors } = validateMappings(testValue as any); + expect(isPlainObject(value)).toBe(true); + expect(errors).toBe(undefined); + }); + }); + + it('should detect valid mappings configuration', () => { + const mappings = { + _source: { + includes: [], + excludes: [], + enabled: true, + }, + _meta: {}, + _routing: { + required: false, + }, + dynamic: true, + }; + + const { errors } = validateMappings(mappings); + expect(errors).toBe(undefined); + }); + + it('should strip out unknown configuration', () => { + const mappings = { + dynamic: true, + date_detection: true, + numeric_detection: true, + dynamic_date_formats: ['abc'], + _source: { + enabled: true, + includes: ['abc'], + excludes: ['abc'], + }, + properties: { title: { type: 'text' } }, + dynamic_templates: [], + unknown: 123, + }; + + const { value, errors } = validateMappings(mappings); + + const { unknown, ...expected } = mappings; + expect(value).toEqual(expected); + expect(errors).toEqual([{ code: 'ERR_CONFIG', configName: 'unknown' }]); + }); + + it('should strip out invalid configuration and returns the errors for each of them', () => { + const mappings = { + dynamic: true, + numeric_detection: 123, // wrong format + dynamic_date_formats: false, // wrong format + _source: { + enabled: true, + unknownProp: 'abc', // invalid + excludes: ['abc'], + }, + properties: 'abc', + }; + + const { value, errors } = validateMappings(mappings); + + expect(value).toEqual({ + dynamic: true, + properties: {}, + dynamic_templates: [], + }); + + expect(errors).not.toBe(undefined); + expect(errors!).toEqual([ + { code: 'ERR_CONFIG', configName: '_source' }, + { code: 'ERR_CONFIG', configName: 'dynamic_date_formats' }, + { code: 'ERR_CONFIG', configName: 'numeric_detection' }, + ]); + }); +}); + +describe('Properties validator', () => { + it('should convert non object to empty object', () => { + const tests = ['abc', 123, [], null, undefined]; + + tests.forEach(testValue => { + const { value, errors } = validateProperties(testValue as any); + expect(isPlainObject(value)).toBe(true); + expect(errors).toEqual([]); + }); + }); + + it('should strip non object fields', () => { + const properties = { + prop1: { type: 'text' }, + prop2: 'abc', // To be removed + prop3: 123, // To be removed + prop4: null, // To be removed + prop5: [], // To be removed + prop6: { + properties: { + prop1: { type: 'text' }, + prop2: 'abc', // To be removed + }, + }, + }; + const { value, errors } = validateProperties(properties as any); + + expect(value).toEqual({ + prop1: { type: 'text' }, + prop6: { + type: 'object', + properties: { + prop1: { type: 'text' }, + }, + }, + }); + + expect(errors).toEqual( + ['prop2', 'prop3', 'prop4', 'prop5', 'prop6.prop2'].map(fieldPath => ({ + code: 'ERR_FIELD', + fieldPath, + })) + ); + }); + + it(`should set the type to "object" when type is not provided`, () => { + const properties = { + prop1: { type: 'text' }, + prop2: {}, + prop3: { + type: 'object', + properties: { + prop1: {}, + prop2: { type: 'keyword' }, + }, + }, + }; + const { value, errors } = validateProperties(properties as any); + + expect(value).toEqual({ + prop1: { + type: 'text', + }, + prop2: { + type: 'object', + }, + prop3: { + type: 'object', + properties: { + prop1: { + type: 'object', + }, + prop2: { + type: 'keyword', + }, + }, + }, + }); + expect(errors).toEqual([]); + }); + + it('should strip field whose type is not a string or is unknown', () => { + const properties = { + prop1: { type: 123 }, + prop2: { type: 'clearlyUnknown' }, + }; + + const { value, errors } = validateProperties(properties as any); + + expect(Object.keys(value)).toEqual([]); + expect(errors).toEqual([ + { + code: 'ERR_FIELD', + fieldPath: 'prop1', + }, + { + code: 'ERR_FIELD', + fieldPath: 'prop2', + }, + ]); + }); + + it('should strip parameters that are unknown', () => { + const properties = { + prop1: { type: 'text', unknown: true, anotherUnknown: 123 }, + prop2: { type: 'keyword', store: true, index: true, doc_values_binary: true }, + prop3: { + type: 'object', + properties: { + hello: { type: 'keyword', unknown: true, anotherUnknown: 123 }, + }, + }, + }; + + const { value, errors } = validateProperties(properties as any); + + expect(value).toEqual({ + prop1: { type: 'text' }, + prop2: { type: 'keyword', store: true, index: true, doc_values_binary: true }, + prop3: { + type: 'object', + properties: { + hello: { type: 'keyword' }, + }, + }, + }); + + expect(errors).toEqual([ + { code: 'ERR_PARAMETER', fieldPath: 'prop1', paramName: 'unknown' }, + { code: 'ERR_PARAMETER', fieldPath: 'prop1', paramName: 'anotherUnknown' }, + { code: 'ERR_PARAMETER', fieldPath: 'prop3.hello', paramName: 'unknown' }, + { code: 'ERR_PARAMETER', fieldPath: 'prop3.hello', paramName: 'anotherUnknown' }, + ]); + }); + + it(`should strip parameters whose value don't have the valid type.`, () => { + const properties = { + // All the parameters in "wrongField" have a wrong format defined + // and should be stripped out when running the validation + wrongField: { + type: 'text', + store: 'abc', + index: 'abc', + doc_values: { a: 123 }, + doc_values_binary: null, + fielddata: [''], + fielddata_frequency_filter: [123, 456], + coerce: 1234, + coerce_shape: '', + ignore_malformed: 0, + null_value_numeric: 'abc', + null_value_boolean: [], + copy_to: [], + max_input_length: true, + locale: 1, + orientation: [], + boost: { a: 123 }, + scaling_factor: 'some_string', + dynamic: [true], + enabled: 'false', + format: null, + analyzer: 1, + search_analyzer: null, + search_quote_analyzer: {}, + normalizer: [], + index_options: 1, + index_options_keyword: true, + index_options_flattened: [], + eager_global_ordinals: 123, + index_phrases: null, + preserve_separators: 'abc', + preserve_position_increments: [], + ignore_z_value: {}, + points_only: [true], + norms: 'false', + norms_keyword: 'abc', + term_vector: ['no'], + path: [null], + position_increment_gap: 'abc', + index_prefixes: { min_chars: [], max_chars: 'abc' }, + similarity: 1, + split_queries_on_whitespace: {}, + ignore_above: 'abc', + enable_position_increments: [], + depth_limit: true, + dims: false, + max_shingle_size: 'string_not_allowed', + }, + // All the parameters in "goodField" have the correct format + // and should still be there after the validation ran. + goodField: { + type: 'text', + store: true, + index: true, + doc_values: true, + doc_values_binary: true, + fielddata: true, + fielddata_frequency_filter: { min: 1, max: 2, min_segment_size: 10 }, + coerce: true, + coerce_shape: true, + ignore_malformed: true, + null_value: 'NULL', + null_value_numeric: 1, + null_value_boolean: 'true', + copy_to: 'abc', + max_input_length: 10, + locale: 'en', + orientation: 'ccw', + boost: 1.5, + scaling_factor: 2.5, + dynamic: 'strict', // true | false | 'strict' are allowed + enabled: true, + format: 'strict_date_optional_time', + analyzer: 'standard', + search_analyzer: 'standard', + search_quote_analyzer: 'standard', + normalizer: 'standard', + index_options: 'positions', + index_options_keyword: 'docs', + index_options_flattened: 'docs', + eager_global_ordinals: true, + index_phrases: true, + preserve_separators: true, + preserve_position_increments: true, + ignore_z_value: true, + points_only: true, + norms: true, + norms_keyword: true, + term_vector: 'no', + path: 'abc', + position_increment_gap: 100, + index_prefixes: { min_chars: 2, max_chars: 5 }, + similarity: 'BM25', + split_queries_on_whitespace: true, + ignore_above: 64, + enable_position_increments: true, + depth_limit: 20, + dims: 'abc', + max_shingle_size: 2, + }, + goodField2: { + type: 'object', + dynamic: true, + }, + goodField3: { + type: 'object', + dynamic: false, + }, + }; + + const { value, errors } = validateProperties(properties as any); + + expect(Object.keys(value)).toEqual(['wrongField', 'goodField', 'goodField2', 'goodField3']); + + expect(value.wrongField).toEqual({ type: 'text' }); // All parameters have been stripped out but the "type". + expect(value.goodField).toEqual(properties.goodField); // All parameters are stil there. + expect(value.goodField2).toEqual(properties.goodField2); + expect(value.goodField3).toEqual(properties.goodField3); + + const allWrongParameters = Object.keys(properties.wrongField).filter(v => v !== 'type'); + expect(errors).toEqual( + allWrongParameters.map(paramName => ({ + code: 'ERR_PARAMETER', + fieldPath: 'wrongField', + paramName, + })) + ); + }); +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts new file mode 100644 index 0000000000000..fb0b7560232ee --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts @@ -0,0 +1,311 @@ +/* + * 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 { pick, isPlainObject } from 'lodash'; +import * as t from 'io-ts'; +import { ordString } from 'fp-ts/lib/Ord'; +import { toArray } from 'fp-ts/lib/Set'; +import { isLeft, isRight } from 'fp-ts/lib/Either'; + +import { errorReporter } from './error_reporter'; +import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants'; +import { FieldMeta } from '../types'; +import { getFieldMeta } from './utils'; + +const ALLOWED_FIELD_PROPERTIES = [ + ...Object.keys(PARAMETERS_DEFINITION), + 'type', + 'properties', + 'fields', +]; + +const DEFAULT_FIELD_TYPE = 'object'; + +export type MappingsValidationError = + | { code: 'ERR_CONFIG'; configName: string } + | { code: 'ERR_FIELD'; fieldPath: string } + | { code: 'ERR_PARAMETER'; paramName: string; fieldPath: string }; + +export interface MappingsValidatorResponse { + /* The parsed mappings object without any error */ + value: GenericObject; + errors?: MappingsValidationError[]; +} + +interface PropertiesValidatorResponse { + /* The parsed "properties" object without any error */ + value: GenericObject; + errors: MappingsValidationError[]; +} + +interface FieldValidatorResponse { + /* The parsed field. If undefined means that it was invalid */ + value?: GenericObject; + parametersRemoved: string[]; +} + +interface GenericObject { + [key: string]: any; +} + +const validateFieldType = (type: any): boolean => { + if (typeof type !== 'string') { + return false; + } + + if (!ALL_DATA_TYPES.includes(type)) { + return false; + } + return true; +}; + +const validateParameter = (parameter: string, value: any): boolean => { + if (parameter === 'type') { + return true; + } + + if (parameter === 'name') { + return false; + } + + if (parameter === 'properties' || parameter === 'fields') { + return isPlainObject(value); + } + + const parameterSchema = (PARAMETERS_DEFINITION as any)[parameter]!.schema; + if (parameterSchema) { + return isRight(parameterSchema.decode(value)); + } + + // Fallback, if no schema defined for the parameter (this should not happen in theory) + return true; +}; + +const stripUnknownOrInvalidParameter = (field: GenericObject): FieldValidatorResponse => + Object.entries(field).reduce( + (acc, [key, value]) => { + if (!ALLOWED_FIELD_PROPERTIES.includes(key) || !validateParameter(key, value)) { + acc.parametersRemoved.push(key); + } else { + acc.value = acc.value !== undefined && acc.value !== null ? acc.value : {}; + acc.value[key] = value; + } + return acc; + }, + { parametersRemoved: [] } as FieldValidatorResponse + ); + +const parseField = (field: any): FieldValidatorResponse & { meta?: FieldMeta } => { + // Sanitize the input to make sure we are working with an object + if (!isPlainObject(field)) { + return { parametersRemoved: [] }; + } + // Make sure the field "type" is valid + if ( + !validateFieldType( + field.type !== undefined && field.type !== null ? field.type : DEFAULT_FIELD_TYPE + ) + ) { + return { parametersRemoved: [] }; + } + + // Filter out unknown or invalid "parameters" + const fieldWithType = { type: DEFAULT_FIELD_TYPE, ...field }; + const parsedField = stripUnknownOrInvalidParameter(fieldWithType); + const meta = getFieldMeta(fieldWithType); + + return { ...parsedField, meta }; +}; + +const parseFields = ( + properties: GenericObject, + path: string[] = [] +): PropertiesValidatorResponse => { + return Object.entries(properties).reduce( + (acc, [fieldName, unparsedField]) => { + const fieldPath = [...path, fieldName].join('.'); + const { value: parsedField, parametersRemoved, meta } = parseField(unparsedField); + + if (parsedField === undefined) { + // Field has been stripped out because it was invalid + acc.errors.push({ code: 'ERR_FIELD', fieldPath }); + } else { + if (meta!.hasChildFields || meta!.hasMultiFields) { + // Recursively parse all the possible children ("properties" or "fields" for multi-fields) + const parsedChildren = parseFields(parsedField[meta!.childFieldsName!], [ + ...path, + fieldName, + ]); + parsedField[meta!.childFieldsName!] = parsedChildren.value; + + /** + * If the children parsed have any error we concatenate them in our accumulator. + */ + if (parsedChildren.errors) { + acc.errors = [...acc.errors, ...parsedChildren.errors]; + } + } + + acc.value[fieldName] = parsedField; + + if (Boolean(parametersRemoved.length)) { + acc.errors = [ + ...acc.errors, + ...parametersRemoved.map(paramName => ({ + code: 'ERR_PARAMETER' as 'ERR_PARAMETER', + fieldPath, + paramName, + })), + ]; + } + } + + return acc; + }, + { + value: {}, + errors: [], + } as PropertiesValidatorResponse + ); +}; + +/** + * Utility function that reads a mappings "properties" object and validate its fields by + * - Removing unknown field types + * - Removing unknown field parameters or field parameters that don't have the correct format. + * + * This method does not mutate the original properties object. It returns an object with + * the parsed properties and an array of field paths that have been removed. + * This allows us to display a warning in the UI and let the user correct the fields that we + * are about to remove. + * + * NOTE: The Joi Schema that we defined for each parameter (in "parameters_definition".tsx) + * does not do an exhaustive validation of the parameter value. + * It's main purpose is to prevent the UI from blowing up. + * + * @param properties A mappings "properties" object + */ +export const validateProperties = (properties = {}): PropertiesValidatorResponse => { + // Sanitize the input to make sure we are working with an object + if (!isPlainObject(properties)) { + return { value: {}, errors: [] }; + } + + return parseFields(properties); +}; + +/** + * Single source of truth to validate the *configuration* of the mappings. + * Whenever a user loads a JSON object it will be validate against this Joi schema. + */ +export const mappingsConfigurationSchema = t.exact( + t.partial({ + dynamic: t.union([t.literal(true), t.literal(false), t.literal('strict')]), + date_detection: t.boolean, + numeric_detection: t.boolean, + dynamic_date_formats: t.array(t.string), + _source: t.exact( + t.partial({ + enabled: t.boolean, + includes: t.array(t.string), + excludes: t.array(t.string), + }) + ), + _meta: t.UnknownRecord, + _routing: t.interface({ + required: t.boolean, + }), + }) +); + +const mappingsConfigurationSchemaKeys = Object.keys(mappingsConfigurationSchema.type.props); +const sourceConfigurationSchemaKeys = Object.keys( + mappingsConfigurationSchema.type.props._source.type.props +); + +export const validateMappingsConfiguration = ( + mappingsConfiguration: any +): { value: any; errors: MappingsValidationError[] } => { + // Set to keep track of invalid configuration parameters. + const configurationRemoved: Set = new Set(); + + let copyOfMappingsConfig = { ...mappingsConfiguration }; + const result = mappingsConfigurationSchema.decode(mappingsConfiguration); + const isSchemaInvalid = isLeft(result); + + const unknownConfigurationParameters = Object.keys(mappingsConfiguration).filter( + key => mappingsConfigurationSchemaKeys.includes(key) === false + ); + + const unknownSourceConfigurationParameters = + mappingsConfiguration._source !== undefined + ? Object.keys(mappingsConfiguration._source).filter( + key => sourceConfigurationSchemaKeys.includes(key) === false + ) + : []; + + if (isSchemaInvalid) { + /** + * To keep the logic simple we will strip out the parameters that contain errors + */ + const errors = errorReporter.report(result); + errors.forEach(error => { + const configurationName = error.path[0]; + configurationRemoved.add(configurationName); + delete copyOfMappingsConfig[configurationName]; + }); + } + + if (unknownConfigurationParameters.length > 0) { + unknownConfigurationParameters.forEach(configName => configurationRemoved.add(configName)); + } + + if (unknownSourceConfigurationParameters.length > 0) { + configurationRemoved.add('_source'); + delete copyOfMappingsConfig._source; + } + + copyOfMappingsConfig = pick(copyOfMappingsConfig, mappingsConfigurationSchemaKeys); + + const errors: MappingsValidationError[] = toArray(ordString)(configurationRemoved) + .map(configName => ({ + code: 'ERR_CONFIG', + configName, + })) + .sort((a, b) => a.configName.localeCompare(b.configName)) as MappingsValidationError[]; + + return { value: copyOfMappingsConfig, errors }; +}; + +export const validateMappings = (mappings: any = {}): MappingsValidatorResponse => { + if (!isPlainObject(mappings)) { + return { value: {} }; + } + + const { properties, dynamic_templates: dynamicTemplates, ...mappingsConfiguration } = mappings; + + const { value: parsedConfiguration, errors: configurationErrors } = validateMappingsConfiguration( + mappingsConfiguration + ); + const { value: parsedProperties, errors: propertiesErrors } = validateProperties(properties); + + const errors = [...configurationErrors, ...propertiesErrors]; + + return { + value: { + ...parsedConfiguration, + properties: parsedProperties, + dynamic_templates: + dynamicTemplates !== undefined && dynamicTemplates !== null ? dynamicTemplates : [], + }, + errors: errors.length ? errors : undefined, + }; +}; + +export const VALID_MAPPINGS_PARAMETERS = [ + ...mappingsConfigurationSchemaKeys, + 'dynamic_templates', + 'properties', +]; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts new file mode 100644 index 0000000000000..0431ea472643b --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts @@ -0,0 +1,65 @@ +/* + * 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. + */ + +jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} })); + +import { isStateValid } from './utils'; + +describe('utils', () => { + describe('isStateValid()', () => { + let components: any; + it('handles base case', () => { + components = { + fieldsJsonEditor: { isValid: undefined }, + configuration: { isValid: undefined }, + fieldForm: undefined, + }; + expect(isStateValid(components)).toBe(undefined); + }); + + it('handles combinations of true, false and undefined', () => { + components = { + fieldsJsonEditor: { isValid: false }, + configuration: { isValid: true }, + fieldForm: undefined, + }; + + expect(isStateValid(components)).toBe(false); + + components = { + fieldsJsonEditor: { isValid: false }, + configuration: { isValid: undefined }, + fieldForm: undefined, + }; + + expect(isStateValid(components)).toBe(undefined); + + components = { + fieldsJsonEditor: { isValid: true }, + configuration: { isValid: undefined }, + fieldForm: undefined, + }; + + expect(isStateValid(components)).toBe(undefined); + + components = { + fieldsJsonEditor: { isValid: true }, + configuration: { isValid: false }, + fieldForm: undefined, + }; + + expect(isStateValid(components)).toBe(false); + + components = { + fieldsJsonEditor: { isValid: false }, + configuration: { isValid: true }, + fieldForm: { isValid: true }, + }; + + expect(isStateValid(components)).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts new file mode 100644 index 0000000000000..b14a5f0d88fe9 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -0,0 +1,487 @@ +/* + * 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 uuid from 'uuid'; + +import { + DataType, + Fields, + Field, + NormalizedFields, + NormalizedField, + FieldMeta, + MainType, + SubType, + ChildFieldName, + ComboBoxOption, +} from '../types'; + +import { + SUB_TYPE_MAP_TO_MAIN, + MAX_DEPTH_DEFAULT_EDITOR, + TYPE_NOT_ALLOWED_MULTIFIELD, + TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL, +} from '../constants'; + +export const getUniqueId = () => { + return uuid.v4(); +}; + +const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { + if (dataType === 'text' || dataType === 'keyword') { + return 'fields'; + } else if (dataType === 'object' || dataType === 'nested') { + return 'properties'; + } + return undefined; +}; + +export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => { + const childFieldsName = getChildFieldsName(field.type); + + const canHaveChildFields = isMultiField ? false : childFieldsName === 'properties'; + const hasChildFields = isMultiField + ? false + : canHaveChildFields && + Boolean(field[childFieldsName!]) && + Object.keys(field[childFieldsName!]!).length > 0; + + const canHaveMultiFields = isMultiField ? false : childFieldsName === 'fields'; + const hasMultiFields = isMultiField + ? false + : canHaveMultiFields && + Boolean(field[childFieldsName!]) && + Object.keys(field[childFieldsName!]!).length > 0; + + return { + childFieldsName, + canHaveChildFields, + hasChildFields, + canHaveMultiFields, + hasMultiFields, + isExpanded: false, + }; +}; + +/** + * For "alias" field types, we work internaly by "id" references. When we normalize the fields, we need to + * replace the actual "path" parameter with the field (internal) `id` the alias points to. + * This method takes care of doing just that. + * + * @param byId The fields map by id + */ + +const replaceAliasPathByAliasId = ( + byId: NormalizedFields['byId'] +): { + aliases: NormalizedFields['aliases']; + byId: NormalizedFields['byId']; +} => { + const aliases: NormalizedFields['aliases'] = {}; + + Object.entries(byId).forEach(([id, field]) => { + if (field.source.type === 'alias') { + const aliasTargetField = Object.values(byId).find( + _field => _field.path.join('.') === field.source.path + ); + + if (aliasTargetField) { + // we set the path to the aliasTargetField "id" + field.source.path = aliasTargetField.id; + + // We add the alias field to our "aliases" map + aliases[aliasTargetField.id] = aliases[aliasTargetField.id] || []; + aliases[aliasTargetField.id].push(id); + } + } + }); + + return { aliases, byId }; +}; + +export const getMainTypeFromSubType = (subType: SubType): MainType => + SUB_TYPE_MAP_TO_MAIN[subType] as MainType; + +/** + * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields + * to a `byId` object where the key is the **path** to the field and the value is a `NormalizedField`. + * The `NormalizedField` contains the field data under `source` and meta information about the capability of the field. + * + * @example + +// original +{ + myObject: { + type: 'object', + properties: { + name: { + type: 'text' + } + } + } +} + +// normalized +{ + rootLevelFields: ['_uniqueId123'], + byId: { + '_uniqueId123': { + source: { type: 'object' }, + id: '_uniqueId123', + parentId: undefined, + hasChildFields: true, + childFieldsName: 'properties', // "object" type have their child fields under "properties" + canHaveChildFields: true, + childFields: ['_uniqueId456'], + }, + '_uniqueId456': { + source: { type: 'text' }, + id: '_uniqueId456', + parentId: '_uniqueId123', + hasChildFields: false, + childFieldsName: 'fields', // "text" type have their child fields under "fields" + canHaveChildFields: true, + childFields: undefined, + }, + }, +} + * + * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types) + */ +export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { + let maxNestedDepth = 0; + + const normalizeFields = ( + props: Fields, + to: NormalizedFields['byId'], + paths: string[], + arrayToKeepRef: string[], + nestedDepth: number, + isMultiField: boolean = false, + parentId?: string + ): Record => + Object.entries(props) + .sort(([a], [b]) => (a > b ? 1 : a < b ? -1 : 0)) + .reduce((acc, [propName, value]) => { + const id = getUniqueId(); + arrayToKeepRef.push(id); + const field = { name: propName, ...value } as Field; + + // In some cases for object, the "type" is not defined but the field + // has properties defined. The mappings editor requires a "type" to be defined + // so we add it here. + if (field.type === undefined && field.properties !== undefined) { + field.type = 'object'; + } + + const meta = getFieldMeta(field, isMultiField); + const { childFieldsName, hasChildFields, hasMultiFields } = meta; + + if (hasChildFields || hasMultiFields) { + const nextDepth = + meta.canHaveChildFields || meta.canHaveMultiFields ? nestedDepth + 1 : nestedDepth; + meta.childFields = []; + maxNestedDepth = Math.max(maxNestedDepth, nextDepth); + + normalizeFields( + field[childFieldsName!]!, + to, + [...paths, propName], + meta.childFields, + nextDepth, + meta.canHaveMultiFields, + id + ); + } + + const { properties, fields, ...rest } = field; + + const normalizedField: NormalizedField = { + id, + parentId, + nestedDepth, + isMultiField, + path: paths.length ? [...paths, propName] : [propName], + source: rest, + ...meta, + }; + + acc[id] = normalizedField; + + return acc; + }, to); + + const rootLevelFields: string[] = []; + const { byId, aliases } = replaceAliasPathByAliasId( + normalizeFields(fieldsToNormalize, {}, [], rootLevelFields, 0) + ); + + return { + byId, + aliases, + rootLevelFields, + maxNestedDepth, + }; +}; + +/** + * The alias "path" value internally point to a field "id" (not its path). When we deNormalize the fields, + * we need to replace the target field "id" by its actual "path", making sure to not mutate our state "fields" object. + * + * @param aliases The aliases map + * @param byId The fields map by id + */ +const replaceAliasIdByAliasPath = ( + aliases: NormalizedFields['aliases'], + byId: NormalizedFields['byId'] +): NormalizedFields['byId'] => { + const updatedById = { ...byId }; + + Object.entries(aliases).forEach(([targetId, aliasesIds]) => { + const path = updatedById[targetId] ? updatedById[targetId].path.join('.') : ''; + + aliasesIds.forEach(id => { + const aliasField = updatedById[id]; + if (!aliasField) { + return; + } + const fieldWithUpdatedPath: NormalizedField = { + ...aliasField, + source: { ...aliasField.source, path }, + }; + + updatedById[id] = fieldWithUpdatedPath; + }); + }); + + return updatedById; +}; + +export const deNormalize = ({ rootLevelFields, byId, aliases }: NormalizedFields): Fields => { + const serializedFieldsById = replaceAliasIdByAliasPath(aliases, byId); + + const deNormalizePaths = (ids: string[], to: Fields = {}) => { + ids.forEach(id => { + const { source, childFields, childFieldsName } = serializedFieldsById[id]; + const { name, ...normalizedField } = source; + const field: Omit = normalizedField; + to[name] = field; + if (childFields) { + field[childFieldsName!] = {}; + return deNormalizePaths(childFields, field[childFieldsName!]); + } + }); + return to; + }; + + return deNormalizePaths(rootLevelFields); +}; + +/** + * If we change the "name" of a field, we need to update its `path` and the + * one of **all** of its child properties or multi-fields. + * + * @param field The field who's name has changed + * @param byId The map of all the document fields + */ +export const updateFieldsPathAfterFieldNameChange = ( + field: NormalizedField, + byId: NormalizedFields['byId'] +): { updatedFieldPath: string[]; updatedById: NormalizedFields['byId'] } => { + const updatedById = { ...byId }; + const paths = field.parentId ? byId[field.parentId].path : []; + + const updateFieldPath = (_field: NormalizedField, _paths: string[]): void => { + const { name } = _field.source; + const path = _paths.length === 0 ? [name] : [..._paths, name]; + + updatedById[_field.id] = { + ..._field, + path, + }; + + if (_field.hasChildFields || _field.hasMultiFields) { + _field + .childFields!.map(fieldId => byId[fieldId]) + .forEach(childField => { + updateFieldPath(childField, [..._paths, name]); + }); + } + }; + + updateFieldPath(field, paths); + + return { updatedFieldPath: updatedById[field.id].path, updatedById }; +}; + +/** + * Retrieve recursively all the children fields of a field + * + * @param field The field to return the children from + * @param byId Map of all the document fields + */ +export const getAllChildFields = ( + field: NormalizedField, + byId: NormalizedFields['byId'] +): NormalizedField[] => { + const getChildFields = (_field: NormalizedField, to: NormalizedField[] = []) => { + if (_field.hasChildFields || _field.hasMultiFields) { + _field + .childFields!.map(fieldId => byId[fieldId]) + .forEach(childField => { + to.push(childField); + getChildFields(childField, to); + }); + } + return to; + }; + + return getChildFields(field); +}; + +/** + * If we delete an object with child fields or a text/keyword with multi-field, + * we need to know if any of its "child" fields has an `alias` that points to it. + * This method traverse the field descendant tree and returns all the aliases found + * on the field and its possible children. + */ +export const getAllDescendantAliases = ( + field: NormalizedField, + fields: NormalizedFields, + aliasesIds: string[] = [] +): string[] => { + const hasAliases = fields.aliases[field.id] && Boolean(fields.aliases[field.id].length); + + if (!hasAliases && !field.hasChildFields && !field.hasMultiFields) { + return aliasesIds; + } + + if (hasAliases) { + fields.aliases[field.id].forEach(id => { + aliasesIds.push(id); + }); + } + + if (field.childFields) { + field.childFields.forEach(id => { + if (!fields.byId[id]) { + return; + } + getAllDescendantAliases(fields.byId[id], fields, aliasesIds); + }); + } + + return aliasesIds; +}; + +/** + * Helper to retrieve a map of all the ancestors of a field + * + * @param fieldId The field id + * @param byId A map of all the fields by Id + */ +export const getFieldAncestors = ( + fieldId: string, + byId: NormalizedFields['byId'] +): { [key: string]: boolean } => { + const ancestors: { [key: string]: boolean } = {}; + const currentField = byId[fieldId]; + let parent: NormalizedField | undefined = + currentField.parentId === undefined ? undefined : byId[currentField.parentId]; + + while (parent) { + ancestors[parent.id] = true; + parent = parent.parentId === undefined ? undefined : byId[parent.parentId]; + } + + return ancestors; +}; + +export const filterTypesForMultiField = ( + options: ComboBoxOption[] +): ComboBoxOption[] => + options.filter( + option => TYPE_NOT_ALLOWED_MULTIFIELD.includes(option.value as MainType) === false + ); + +export const filterTypesForNonRootFields = ( + options: ComboBoxOption[] +): ComboBoxOption[] => + options.filter( + option => TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL.includes(option.value as MainType) === false + ); + +/** + * Return the max nested depth of the document fields + * + * @param byId Map of all the document fields + */ +export const getMaxNestedDepth = (byId: NormalizedFields['byId']): number => + Object.values(byId).reduce((maxDepth, field) => { + return Math.max(maxDepth, field.nestedDepth); + }, 0); + +/** + * Create a nested array of fields and its possible children + * to render a Tree view of them. + */ +export const buildFieldTreeFromIds = ( + fieldsIds: string[], + byId: NormalizedFields['byId'], + render: (field: NormalizedField) => JSX.Element | string +): any[] => + fieldsIds.map(id => { + const field = byId[id]; + const children = field.childFields + ? buildFieldTreeFromIds(field.childFields, byId, render) + : undefined; + + return { label: render(field), children }; + }); + +/** + * When changing the type of a field, in most cases we want to delete all its child fields. + * There are some exceptions, when changing from "text" to "keyword" as both have the same "fields" property. + */ +export const shouldDeleteChildFieldsAfterTypeChange = ( + oldType: DataType, + newType: DataType +): boolean => { + if (oldType === 'text' && newType !== 'keyword') { + return true; + } else if (oldType === 'keyword' && newType !== 'text') { + return true; + } else if (oldType === 'object' && newType !== 'nested') { + return true; + } else if (oldType === 'nested' && newType !== 'object') { + return true; + } + + return false; +}; + +export const canUseMappingsEditor = (maxNestedDepth: number) => + maxNestedDepth < MAX_DEPTH_DEFAULT_EDITOR; + +const stateWithValidity = ['configuration', 'fieldsJsonEditor', 'fieldForm']; + +export const isStateValid = (state: { [key: string]: any }): boolean | undefined => + Object.entries(state) + .filter(([key]) => stateWithValidity.includes(key)) + .reduce( + (isValid, { 1: value }) => { + if (value === undefined) { + return isValid; + } + + // If one section validity of the state is "undefined", the mappings validity is also "undefined" + if (isValid === undefined || value.isValid === undefined) { + return undefined; + } + + return isValid && value.isValid; + }, + true as undefined | boolean + ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts new file mode 100644 index 0000000000000..dbbffe5a0bd31 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -0,0 +1,293 @@ +/* + * 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 { ReactNode, OptionHTMLAttributes } from 'react'; + +import { FieldConfig } from './shared_imports'; +import { PARAMETERS_DEFINITION } from './constants'; + +export interface DataTypeDefinition { + label: string; + value: DataType; + documentation?: { + main: string; + [key: string]: string; + }; + subTypes?: { label: string; types: SubType[] }; + description?: () => ReactNode; +} + +export interface ParameterDefinition { + title?: string; + description?: JSX.Element | string; + fieldConfig: FieldConfig; + schema?: any; + props?: { [key: string]: ParameterDefinition }; + documentation?: { + main: string; + [key: string]: string; + }; + [key: string]: any; +} + +export type MainType = + | 'text' + | 'keyword' + | 'numeric' + | 'binary' + | 'boolean' + | 'range' + | 'object' + | 'nested' + | 'alias' + | 'completion' + | 'dense_vector' + | 'flattened' + | 'ip' + | 'join' + | 'percolator' + | 'rank_feature' + | 'rank_features' + | 'shape' + | 'search_as_you_type' + | 'date' + | 'date_nanos' + | 'geo_point' + | 'geo_shape' + | 'token_count'; + +export type SubType = NumericType | RangeType; + +export type DataType = MainType | SubType; + +export type NumericType = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'double' + | 'float' + | 'half_float' + | 'scaled_float'; + +export type RangeType = + | 'integer_range' + | 'float_range' + | 'long_range' + | 'ip_range' + | 'double_range' + | 'date_range'; + +export type ParameterName = + | 'name' + | 'type' + | 'store' + | 'index' + | 'fielddata' + | 'fielddata_frequency_filter' + | 'fielddata_frequency_filter_percentage' + | 'fielddata_frequency_filter_absolute' + | 'doc_values' + | 'doc_values_binary' + | 'coerce' + | 'coerce_shape' + | 'ignore_malformed' + | 'null_value' + | 'null_value_numeric' + | 'null_value_boolean' + | 'null_value_geo_point' + | 'null_value_ip' + | 'copy_to' + | 'dynamic' + | 'dynamic_toggle' + | 'dynamic_strict' + | 'enabled' + | 'boost' + | 'locale' + | 'format' + | 'analyzer' + | 'search_analyzer' + | 'search_quote_analyzer' + | 'index_options' + | 'index_options_flattened' + | 'index_options_keyword' + | 'eager_global_ordinals' + | 'eager_global_ordinals_join' + | 'index_prefixes' + | 'index_phrases' + | 'norms' + | 'norms_keyword' + | 'term_vector' + | 'position_increment_gap' + | 'similarity' + | 'normalizer' + | 'ignore_above' + | 'split_queries_on_whitespace' + | 'scaling_factor' + | 'max_input_length' + | 'preserve_separators' + | 'preserve_position_increments' + | 'ignore_z_value' + | 'enable_position_increments' + | 'orientation' + | 'points_only' + | 'path' + | 'dims' + | 'depth_limit' + | 'relations' + | 'max_shingle_size'; + +export interface Parameter { + fieldConfig: FieldConfig; + paramName?: string; + docs?: string; + props?: { [key: string]: FieldConfig }; +} + +export interface Fields { + [key: string]: Omit; +} + +interface FieldBasic { + name: string; + type: DataType; + subType?: SubType; + properties?: { [key: string]: Omit }; + fields?: { [key: string]: Omit }; +} + +type FieldParams = { + [K in ParameterName]: typeof PARAMETERS_DEFINITION[K]['fieldConfig']['defaultValue'] | unknown; +}; + +export type Field = FieldBasic & Partial; + +export interface FieldMeta { + childFieldsName: ChildFieldName | undefined; + canHaveChildFields: boolean; + canHaveMultiFields: boolean; + hasChildFields: boolean; + hasMultiFields: boolean; + childFields?: string[]; + isExpanded: boolean; +} + +export interface NormalizedFields { + byId: { + [id: string]: NormalizedField; + }; + rootLevelFields: string[]; + aliases: { [key: string]: string[] }; + maxNestedDepth: number; +} + +export interface NormalizedField extends FieldMeta { + id: string; + parentId?: string; + nestedDepth: number; + path: string[]; + source: Omit; + isMultiField: boolean; +} + +export type ChildFieldName = 'properties' | 'fields'; + +export type FieldsEditor = 'default' | 'json'; + +export type SelectOption = { + value: unknown; + text: T | ReactNode; +} & OptionHTMLAttributes; + +export interface SuperSelectOption { + value: unknown; + inputDisplay?: ReactNode; + dropdownDisplay?: ReactNode; + disabled?: boolean; + 'data-test-subj'?: string; +} + +export interface AliasOption { + id: string; + label: string; +} + +export interface IndexSettingsInterface { + analysis?: { + analyzer: { + [key: string]: { + type: string; + tokenizer: string; + char_filter?: string[]; + filter?: string[]; + position_increment_gap?: number; + }; + }; + }; +} + +/** + * When we define the index settings we can skip + * the "index" property and directly add the "analysis". + * ES always returns the settings wrapped under "index". + */ +export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface }; + +export interface ComboBoxOption { + label: string; + value?: unknown; +} + +export interface SearchResult { + display: JSX.Element; + field: NormalizedField; +} + +export interface SearchMetadata { + /** + * Whether or not the search term match some part of the field path. + */ + matchPath: boolean; + /** + * If the search term matches the field type we will give it a higher score. + */ + matchType: boolean; + /** + * If the last word of the search terms matches the field name + */ + matchFieldName: boolean; + /** + * If the search term matches the beginning of the path we will give it a higher score + */ + matchStartOfPath: boolean; + /** + * If the last word of the search terms fully matches the field name + */ + fullyMatchFieldName: boolean; + /** + * If the search term exactly matches the field type + */ + fullyMatchType: boolean; + /** + * If the search term matches the full field path + */ + fullyMatchPath: boolean; + /** + * The score of the result that will allow us to sort the list + */ + score: number; + /** + * The JSX with tag wrapping the matched string + */ + display: JSX.Element; + /** + * The field path substring that matches the search + */ + stringMatch: string | null; +} + +export interface GenericObject { + [key: string]: any; +} From 4021fedfdcd675f928b7b48bfc4aff1ec8407bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 24 Jan 2020 12:56:28 +0530 Subject: [PATCH 02/10] Add "include_type_name" parameter to server API req --- .../server/routes/api/mapping/register_mapping_route.js | 1 + .../server/routes/api/templates/register_get_routes.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/mapping/register_mapping_route.js b/x-pack/legacy/plugins/index_management/server/routes/api/mapping/register_mapping_route.js index 790aa21bf9b84..94c36af776d15 100644 --- a/x-pack/legacy/plugins/index_management/server/routes/api/mapping/register_mapping_route.js +++ b/x-pack/legacy/plugins/index_management/server/routes/api/mapping/register_mapping_route.js @@ -15,6 +15,7 @@ const handler = async (request, callWithRequest) => { const params = { expand_wildcards: 'none', index: indexName, + include_type_name: true, }; const hit = await callWithRequest('indices.getMapping', params); diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts index b450f75d1cc53..555feafa053d1 100644 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -13,7 +13,9 @@ let callWithInternalUser: any; const allHandler: RouterRouteHandler = async (_req, callWithRequest) => { const managedTemplatePrefix = await getManagedTemplatePrefix(callWithInternalUser); - const indexTemplatesByName = await callWithRequest('indices.getTemplate'); + const indexTemplatesByName = await callWithRequest('indices.getTemplate', { + include_type_name: true, + }); return deserializeTemplateList(indexTemplatesByName, managedTemplatePrefix); }; @@ -21,7 +23,10 @@ const allHandler: RouterRouteHandler = async (_req, callWithRequest) => { const oneHandler: RouterRouteHandler = async (req, callWithRequest) => { const { name } = req.params; const managedTemplatePrefix = await getManagedTemplatePrefix(callWithInternalUser); - const indexTemplateByName = await callWithRequest('indices.getTemplate', { name }); + const indexTemplateByName = await callWithRequest('indices.getTemplate', { + name, + include_type_name: true, + }); if (indexTemplateByName[name]) { return deserializeTemplate({ ...indexTemplateByName[name], name }, managedTemplatePrefix); From bf96f5e0920b0565245c815aab7d4fbcd22455c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Wed, 29 Jan 2020 15:13:15 +0530 Subject: [PATCH 03/10] Add "include_type_name" req param to client requests --- .../legacy/plugins/index_management/public/services/api.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/services/api.ts b/x-pack/legacy/plugins/index_management/public/services/api.ts index 4592381cad631..cf0e078f6b27f 100644 --- a/x-pack/legacy/plugins/index_management/public/services/api.ts +++ b/x-pack/legacy/plugins/index_management/public/services/api.ts @@ -38,6 +38,7 @@ import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; import { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; import { useRequest, sendRequest } from './use_request'; import { Template } from '../../common/types'; +import { doMappingsHaveType } from '../components/mappings_editor'; let httpClient: ng.IHttpService; @@ -225,8 +226,9 @@ export function loadIndexTemplate(name: Template['name']) { } export async function saveTemplate(template: Template, isClone?: boolean) { + const includeTypeName = doMappingsHaveType(template.mappings); const result = sendRequest({ - path: `${apiPrefix}/templates`, + path: `${apiPrefix}/templates?include_type_name=${includeTypeName}`, method: 'put', body: template, }); @@ -239,9 +241,10 @@ export async function saveTemplate(template: Template, isClone?: boolean) { } export async function updateTemplate(template: Template) { + const includeTypeName = doMappingsHaveType(template.mappings); const { name } = template; const result = sendRequest({ - path: `${apiPrefix}/templates/${encodeURIComponent(name)}`, + path: `${apiPrefix}/templates/${encodeURIComponent(name)}?include_type_name=${includeTypeName}`, method: 'put', body: template, }); From 4fccfbf4bfedbdc894858cf39864c7f424f8c817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 24 Jan 2020 13:33:13 +0530 Subject: [PATCH 04/10] Forward "include_type_name" api call to ES request call --- .../server/routes/api/templates/register_create_route.ts | 2 ++ .../server/routes/api/templates/register_update_route.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts index e134a97dd029e..8cc9f24f78a52 100644 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -15,6 +15,7 @@ import { serializeTemplate } from '../../../../common/lib'; const handler: RouterRouteHandler = async (req, callWithRequest) => { const template = req.payload as Template; + const { include_type_name } = req.query as any; const serializedTemplate = serializeTemplate(template) as TemplateEs; const { name, order, index_patterns, version, settings, mappings, aliases } = serializedTemplate; @@ -49,6 +50,7 @@ const handler: RouterRouteHandler = async (req, callWithRequest) => { return await callWithRequest('indices.putTemplate', { name, order, + include_type_name, body: { index_patterns, version, diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts index 15590e2acbe71..ccbda2dab9364 100644 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -10,6 +10,7 @@ import { serializeTemplate } from '../../../../common/lib'; const handler: RouterRouteHandler = async (req, callWithRequest) => { const { name } = req.params; + const { include_type_name } = req.query as any; const template = req.payload as Template; const serializedTemplate = serializeTemplate(template) as TemplateEs; @@ -22,6 +23,7 @@ const handler: RouterRouteHandler = async (req, callWithRequest) => { return await callWithRequest('indices.putTemplate', { name, order, + include_type_name, body: { index_patterns, version, From 8e52d99f7485421574be7643461300eea49344ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien?= Date: Fri, 24 Jan 2020 15:13:08 +0530 Subject: [PATCH 05/10] Fix API integration test --- .../apis/management/index_management/mapping.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/mapping.js b/x-pack/test/api_integration/apis/management/index_management/mapping.js index fa0f6e04a7a4d..13eca5bef251a 100644 --- a/x-pack/test/api_integration/apis/management/index_management/mapping.js +++ b/x-pack/test/api_integration/apis/management/index_management/mapping.js @@ -32,7 +32,8 @@ export default function({ getService }) { const { body } = await getIndexMapping(index).expect(200); - expect(body.mapping).to.eql(mappings); + // As, on 7.x we require the mappings with type (include_type_name), the default "_doc" type is returned + expect(body.mapping).to.eql({ _doc: mappings }); }); }); } From f9780a1c357dcd4cc3d440888055a2f58f6ae04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Wed, 29 Jan 2020 17:02:04 +0530 Subject: [PATCH 06/10] Fix TS issue --- .../public/components/mappings_editor/types.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index dbbffe5a0bd31..5f6c2d57e35aa 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -5,9 +5,6 @@ */ import { ReactNode, OptionHTMLAttributes } from 'react'; -import { FieldConfig } from './shared_imports'; -import { PARAMETERS_DEFINITION } from './constants'; - export interface DataTypeDefinition { label: string; value: DataType; @@ -22,7 +19,7 @@ export interface DataTypeDefinition { export interface ParameterDefinition { title?: string; description?: JSX.Element | string; - fieldConfig: FieldConfig; + fieldConfig: any; schema?: any; props?: { [key: string]: ParameterDefinition }; documentation?: { @@ -140,10 +137,10 @@ export type ParameterName = | 'max_shingle_size'; export interface Parameter { - fieldConfig: FieldConfig; + fieldConfig: any; paramName?: string; docs?: string; - props?: { [key: string]: FieldConfig }; + props?: { [key: string]: any }; } export interface Fields { @@ -159,7 +156,7 @@ interface FieldBasic { } type FieldParams = { - [K in ParameterName]: typeof PARAMETERS_DEFINITION[K]['fieldConfig']['defaultValue'] | unknown; + [K in ParameterName]: unknown; }; export type Field = FieldBasic & Partial; From b011fa12bdb389f472476aeb2eff07337127a2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 30 Jan 2020 11:42:08 +0530 Subject: [PATCH 07/10] Clean up unused code --- .../constants/data_types_definition.tsx | 8 +- .../constants/default_values.ts | 14 - .../constants/field_options.tsx | 255 --------- .../constants/field_options_i18n.ts | 495 ------------------ .../mappings_editor/constants/index.ts | 6 - .../constants/mappings_editor.ts | 21 - .../mappings_editor/lib/utils.test.ts | 65 --- .../components/mappings_editor/lib/utils.ts | 447 +--------------- .../components/mappings_editor/types.ts | 138 +---- 9 files changed, 6 insertions(+), 1443 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx index 965057505cd15..7471e85d4754b 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx @@ -5,7 +5,7 @@ */ import { MainType, SubType, DataType, DataTypeDefinition } from '../types'; -export const TYPE_DEFINITION: { [key in DataType]: boolean } = { +const TYPE_DEFINITION: { [key in DataType]: boolean } = { text: true, keyword: true, numeric: true, @@ -46,7 +46,7 @@ export const TYPE_DEFINITION: { [key in DataType]: boolean } = { shape: true, }; -export const MAIN_TYPES: MainType[] = [ +const MAIN_TYPES: MainType[] = [ 'alias', 'binary', 'boolean', @@ -73,7 +73,7 @@ export const MAIN_TYPES: MainType[] = [ 'token_count', ]; -export const MAIN_DATA_TYPE_DEFINITION: { +const MAIN_DATA_TYPE_DEFINITION: { [key in MainType]: DataTypeDefinition; } = MAIN_TYPES.reduce( (acc, type) => ({ @@ -94,7 +94,7 @@ export const MAIN_DATA_TYPE_DEFINITION: { * short: 'numeric', * } */ -export const SUB_TYPE_MAP_TO_MAIN = Object.entries(MAIN_DATA_TYPE_DEFINITION).reduce( +const SUB_TYPE_MAP_TO_MAIN = Object.entries(MAIN_DATA_TYPE_DEFINITION).reduce( (acc, [type, definition]) => { if ({}.hasOwnProperty.call(definition, 'subTypes')) { definition.subTypes!.types.forEach(subType => { diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts deleted file mode 100644 index 96623b855dd3a..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/default_values.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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. - */ - -/** - * When we want to set a parameter value to the index "default" in a Select option - * we will use this constant to define it. We will then strip this placeholder value - * and let Elasticsearch handle it. - */ -export const INDEX_DEFAULT = 'index_default'; - -export const STANDARD = 'standard'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx deleted file mode 100644 index 710e637de8b08..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options.tsx +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 { EuiText } from '@elastic/eui'; - -import { DataType, ParameterName, SelectOption, SuperSelectOption, ComboBoxOption } from '../types'; -import { FIELD_OPTIONS_TEXTS, LANGUAGE_OPTIONS_TEXT, FieldOption } from './field_options_i18n'; -import { INDEX_DEFAULT, STANDARD } from './default_values'; -import { MAIN_DATA_TYPE_DEFINITION } from './data_types_definition'; - -export const TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL: DataType[] = ['join']; - -export const TYPE_NOT_ALLOWED_MULTIFIELD: DataType[] = [ - ...TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL, - 'object', - 'nested', - 'alias', -]; - -export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map( - ([dataType, { label }]) => ({ - value: dataType, - label, - }) -) as ComboBoxOption[]; - -interface SuperSelectOptionConfig { - inputDisplay: string; - dropdownDisplay: JSX.Element; -} - -export const getSuperSelectOption = ( - title: string, - description: string -): SuperSelectOptionConfig => ({ - inputDisplay: title, - dropdownDisplay: ( - <> - {title} - -

{description}

-
- - ), -}); - -const getOptionTexts = (option: FieldOption): SuperSelectOptionConfig => - getSuperSelectOption(FIELD_OPTIONS_TEXTS[option].title, FIELD_OPTIONS_TEXTS[option].description); - -type ParametersOptions = ParameterName | 'languageAnalyzer'; - -export const PARAMETERS_OPTIONS: { - [key in ParametersOptions]?: SelectOption[] | SuperSelectOption[]; -} = { - index_options: [ - { - value: 'docs', - ...getOptionTexts('indexOptions.docs'), - }, - { - value: 'freqs', - ...getOptionTexts('indexOptions.freqs'), - }, - { - value: 'positions', - ...getOptionTexts('indexOptions.positions'), - }, - { - value: 'offsets', - ...getOptionTexts('indexOptions.offsets'), - }, - ] as SuperSelectOption[], - index_options_flattened: [ - { - value: 'docs', - ...getOptionTexts('indexOptions.docs'), - }, - { - value: 'freqs', - ...getOptionTexts('indexOptions.freqs'), - }, - ] as SuperSelectOption[], - index_options_keyword: [ - { - value: 'docs', - ...getOptionTexts('indexOptions.docs'), - }, - { - value: 'freqs', - ...getOptionTexts('indexOptions.freqs'), - }, - ] as SuperSelectOption[], - analyzer: [ - { - value: INDEX_DEFAULT, - ...getOptionTexts('analyzer.indexDefault'), - }, - { - value: STANDARD, - ...getOptionTexts('analyzer.standard'), - }, - { - value: 'simple', - ...getOptionTexts('analyzer.simple'), - }, - { - value: 'whitespace', - ...getOptionTexts('analyzer.whitespace'), - }, - { - value: 'stop', - ...getOptionTexts('analyzer.stop'), - }, - { - value: 'keyword', - ...getOptionTexts('analyzer.keyword'), - }, - { - value: 'pattern', - ...getOptionTexts('analyzer.pattern'), - }, - { - value: 'fingerprint', - ...getOptionTexts('analyzer.fingerprint'), - }, - { - value: 'language', - ...getOptionTexts('analyzer.language'), - }, - ] as SuperSelectOption[], - languageAnalyzer: Object.entries(LANGUAGE_OPTIONS_TEXT).map(([value, text]) => ({ - value, - text, - })), - similarity: [ - { - value: 'BM25', - ...getOptionTexts('similarity.bm25'), - }, - { - value: 'boolean', - ...getOptionTexts('similarity.boolean'), - }, - ] as SuperSelectOption[], - term_vector: [ - { - value: 'no', - ...getOptionTexts('termVector.no'), - }, - { - value: 'yes', - ...getOptionTexts('termVector.yes'), - }, - { - value: 'with_positions', - ...getOptionTexts('termVector.withPositions'), - }, - { - value: 'with_offsets', - ...getOptionTexts('termVector.withOffsets'), - }, - { - value: 'with_positions_offsets', - ...getOptionTexts('termVector.withPositionsOffsets'), - }, - { - value: 'with_positions_payloads', - ...getOptionTexts('termVector.withPositionsPayloads'), - }, - { - value: 'with_positions_offsets_payloads', - ...getOptionTexts('termVector.withPositionsOffsetsPayloads'), - }, - ] as SuperSelectOption[], - orientation: [ - { - value: 'ccw', - ...getOptionTexts('orientation.counterclockwise'), - }, - { - value: 'cw', - ...getOptionTexts('orientation.clockwise'), - }, - ] as SuperSelectOption[], -}; - -const DATE_FORMATS = [ - { label: 'epoch_millis' }, - { label: 'epoch_second' }, - { label: 'date_optional_time', strict: true }, - { label: 'basic_date' }, - { label: 'basic_date_time' }, - { label: 'basic_date_time_no_millis' }, - { label: 'basic_ordinal_date' }, - { label: 'basic_ordinal_date_time' }, - { label: 'basic_ordinal_date_time_no_millis' }, - { label: 'basic_time' }, - { label: 'basic_time_no_millis' }, - { label: 'basic_t_time' }, - { label: 'basic_t_time_no_millis' }, - { label: 'basic_week_date', strict: true }, - { label: 'basic_week_date_time', strict: true }, - { - label: 'basic_week_date_time_no_millis', - strict: true, - }, - { label: 'date', strict: true }, - { label: 'date_hour', strict: true }, - { label: 'date_hour_minute', strict: true }, - { label: 'date_hour_minute_second', strict: true }, - { - label: 'date_hour_minute_second_fraction', - strict: true, - }, - { - label: 'date_hour_minute_second_millis', - strict: true, - }, - { label: 'date_time', strict: true }, - { label: 'date_time_no_millis', strict: true }, - { label: 'hour', strict: true }, - { label: 'hour_minute ', strict: true }, - { label: 'hour_minute_second', strict: true }, - { label: 'hour_minute_second_fraction', strict: true }, - { label: 'hour_minute_second_millis', strict: true }, - { label: 'ordinal_date', strict: true }, - { label: 'ordinal_date_time', strict: true }, - { label: 'ordinal_date_time_no_millis', strict: true }, - { label: 'time', strict: true }, - { label: 'time_no_millis', strict: true }, - { label: 't_time', strict: true }, - { label: 't_time_no_millis', strict: true }, - { label: 'week_date', strict: true }, - { label: 'week_date_time', strict: true }, - { label: 'week_date_time_no_millis', strict: true }, - { label: 'weekyear', strict: true }, - { label: 'weekyear_week', strict: true }, - { label: 'weekyear_week_day', strict: true }, - { label: 'year', strict: true }, - { label: 'year_month', strict: true }, - { label: 'year_month_day', strict: true }, -]; - -const STRICT_DATE_FORMAT_OPTIONS = DATE_FORMATS.filter(format => format.strict).map( - ({ label }) => ({ - label: `strict_${label}`, - }) -); - -const DATE_FORMAT_OPTIONS = DATE_FORMATS.map(({ label }) => ({ label })); - -export const ALL_DATE_FORMAT_OPTIONS = [...DATE_FORMAT_OPTIONS, ...STRICT_DATE_FORMAT_OPTIONS]; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts deleted file mode 100644 index 15079d520f2ad..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/field_options_i18n.ts +++ /dev/null @@ -1,495 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -interface Optioni18n { - title: string; - description: string; -} - -type IndexOptions = - | 'indexOptions.docs' - | 'indexOptions.freqs' - | 'indexOptions.positions' - | 'indexOptions.offsets'; - -type AnalyzerOptions = - | 'analyzer.indexDefault' - | 'analyzer.standard' - | 'analyzer.simple' - | 'analyzer.whitespace' - | 'analyzer.stop' - | 'analyzer.keyword' - | 'analyzer.pattern' - | 'analyzer.fingerprint' - | 'analyzer.language'; - -type SimilarityOptions = 'similarity.bm25' | 'similarity.boolean'; - -type TermVectorOptions = - | 'termVector.no' - | 'termVector.yes' - | 'termVector.withPositions' - | 'termVector.withOffsets' - | 'termVector.withPositionsOffsets' - | 'termVector.withPositionsPayloads' - | 'termVector.withPositionsOffsetsPayloads'; - -type OrientationOptions = 'orientation.counterclockwise' | 'orientation.clockwise'; - -type LanguageAnalyzerOption = - | 'arabic' - | 'armenian' - | 'basque' - | 'bengali' - | 'brazilian' - | 'bulgarian' - | 'catalan' - | 'cjk' - | 'czech' - | 'danish' - | 'dutch' - | 'english' - | 'finnish' - | 'french' - | 'galician' - | 'german' - | 'greek' - | 'hindi' - | 'hungarian' - | 'indonesian' - | 'irish' - | 'italian' - | 'latvian' - | 'lithuanian' - | 'norwegian' - | 'persian' - | 'portuguese' - | 'romanian' - | 'russian' - | 'sorani' - | 'spanish' - | 'swedish' - | 'turkish' - | 'thai'; - -export type FieldOption = - | IndexOptions - | AnalyzerOptions - | SimilarityOptions - | TermVectorOptions - | OrientationOptions; - -export const FIELD_OPTIONS_TEXTS: { [key in FieldOption]: Optioni18n } = { - 'indexOptions.docs': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.docNumberTitle', { - defaultMessage: 'Doc number', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.docNumberDescription', - { - defaultMessage: - 'Index the doc number only. Used to verify the existence of a term in a field.', - } - ), - }, - 'indexOptions.freqs': { - title: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.termFrequencyTitle', - { - defaultMessage: 'Term frequencies', - } - ), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.termFrequencyDescription', - { - defaultMessage: - 'Index the doc number and term frequencies. Repeated terms score higher than single terms.', - } - ), - }, - 'indexOptions.positions': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.positionsTitle', { - defaultMessage: 'Positions', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.positionsDescription', - { - defaultMessage: - 'Index the doc number, term frequencies, positions, and start and end character offsets. Offsets map the term back to the original string.', - } - ), - }, - 'indexOptions.offsets': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.offsetsTitle', { - defaultMessage: 'Offsets', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.indexOptions.offsetsDescription', - { - defaultMessage: - 'Doc number, term frequencies, positions, and start and end character offsets (which map the term back to the original string) are indexed.', - } - ), - }, - 'analyzer.indexDefault': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.indexDefaultTitle', { - defaultMessage: 'Index default', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.indexDefaultDescription', - { - defaultMessage: 'Use the analyzer defined for the index.', - } - ), - }, - 'analyzer.standard': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.standardTitle', { - defaultMessage: 'Standard', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.standardDescription', - { - defaultMessage: - 'The standard analyzer divides text into terms on word boundaries, as defined by the Unicode Text Segmentation algorithm.', - } - ), - }, - 'analyzer.simple': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.simpleTitle', { - defaultMessage: 'Simple', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.simpleDescription', - { - defaultMessage: - 'The simple analyzer divides text into terms whenever it encounters a character which is not a letter. ', - } - ), - }, - 'analyzer.whitespace': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.whitespaceTitle', { - defaultMessage: 'Whitespace', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.whitespaceDescription', - { - defaultMessage: - 'The whitespace analyzer divides text into terms whenever it encounters any whitespace character.', - } - ), - }, - 'analyzer.stop': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.stopTitle', { - defaultMessage: 'Stop', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.stopDescription', - { - defaultMessage: - 'The stop analyzer is like the simple analyzer, but also supports removal of stop words.', - } - ), - }, - 'analyzer.keyword': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.keywordTitle', { - defaultMessage: 'Keyword', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.keywordDescription', - { - defaultMessage: - 'The keyword analyzer is a “noop” analyzer that accepts whatever text it is given and outputs the exact same text as a single term.', - } - ), - }, - 'analyzer.pattern': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.patternTitle', { - defaultMessage: 'Pattern', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.patternDescription', - { - defaultMessage: - 'The pattern analyzer uses a regular expression to split the text into terms. It supports lower-casing and stop words.', - } - ), - }, - 'analyzer.fingerprint': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.fingerprintTitle', { - defaultMessage: 'Fingerprint', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.fingerprintDescription', - { - defaultMessage: - 'The fingerprint analyzer is a specialist analyzer which creates a fingerprint which can be used for duplicate detection.', - } - ), - }, - 'analyzer.language': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.analyzer.languageTitle', { - defaultMessage: 'Language', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.analyzer.languageDescription', - { - defaultMessage: - 'Elasticsearch provides many language-specific analyzers like english or french.', - } - ), - }, - 'similarity.bm25': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.similarity.bm25Title', { - defaultMessage: 'Okapi BM25', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.similarity.bm25Description', - { - defaultMessage: 'The default algorithm used in Elasticsearch and Lucene.', - } - ), - }, - 'similarity.boolean': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.similarity.booleanTitle', { - defaultMessage: 'Boolean', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.similarity.booleanDescription', - { - defaultMessage: - 'A boolean similarity to use when full text-ranking is not needed. The score is based on whether the query terms match.', - } - ), - }, - 'termVector.no': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.noTitle', { - defaultMessage: 'No', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.noDescription', - { - defaultMessage: 'No term vectors are stored.', - } - ), - }, - 'termVector.yes': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.yesTitle', { - defaultMessage: 'Yes', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.yesDescription', - { - defaultMessage: 'Just the terms in the field are stored.', - } - ), - }, - 'termVector.withPositions': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsTitle', { - defaultMessage: 'With positions', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsDescription', - { - defaultMessage: 'Terms and positions are stored.', - } - ), - }, - 'termVector.withOffsets': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.termVector.withOffsetsTitle', { - defaultMessage: 'With offsets', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withOffsetsDescription', - { - defaultMessage: 'Terms and character offsets are stored.', - } - ), - }, - 'termVector.withPositionsOffsets': { - title: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsTitle', - { - defaultMessage: 'With positions and offsets', - } - ), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsDescription', - { - defaultMessage: 'Terms, positions, and character offsets are stored.', - } - ), - }, - 'termVector.withPositionsPayloads': { - title: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsPayloadsTitle', - { - defaultMessage: 'With positions and payloads', - } - ), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsPayloadsDescription', - { - defaultMessage: 'Terms, positions, and payloads are stored.', - } - ), - }, - 'termVector.withPositionsOffsetsPayloads': { - title: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsPayloadsTitle', - { - defaultMessage: 'With positions, offsets, and payloads', - } - ), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.termVector.withPositionsOffsetsPayloadsDescription', - { - defaultMessage: 'Terms, positions, offsets and payloads are stored.', - } - ), - }, - 'orientation.counterclockwise': { - title: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.counterclockwiseTitle', - { - defaultMessage: 'Counterclockwise', - } - ), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.counterclockwiseDescription', - { - defaultMessage: - 'Defines outer polygon vertices in counterclockwise order and interior shape vertices in clockwise order. This is the Open Geospatial Consortium (OGC) and GeoJSON standard.', - } - ), - }, - 'orientation.clockwise': { - title: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.orientation.clockwiseTitle', { - defaultMessage: 'Clockwise', - }), - description: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.orientation.clockwiseDescription', - { - defaultMessage: - 'Defines outer polygon vertices in clockwise order and interior shape vertices in counterclockwise order.', - } - ), - }, -}; - -export const LANGUAGE_OPTIONS_TEXT: { [key in LanguageAnalyzerOption]: string } = { - arabic: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.arabic', { - defaultMessage: 'Arabic', - }), - armenian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.armenian', { - defaultMessage: 'Armenian', - }), - basque: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.basque', { - defaultMessage: 'Basque', - }), - bengali: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.bengali', { - defaultMessage: 'Bengali', - }), - brazilian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.brazilian', { - defaultMessage: 'Brazilian', - }), - bulgarian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.bulgarian', { - defaultMessage: 'Bulgarian', - }), - catalan: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.catalan', { - defaultMessage: 'Catalan', - }), - cjk: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.cjk', { - defaultMessage: 'Cjk', - }), - czech: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.czech', { - defaultMessage: 'Czech', - }), - danish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.danish', { - defaultMessage: 'Danish', - }), - dutch: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.dutch', { - defaultMessage: 'Dutch', - }), - english: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.english', { - defaultMessage: 'English', - }), - finnish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.finnish', { - defaultMessage: 'Finnish', - }), - french: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.french', { - defaultMessage: 'French', - }), - galician: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.galician', { - defaultMessage: 'Galician', - }), - german: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.german', { - defaultMessage: 'German', - }), - greek: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.greek', { - defaultMessage: 'Greek', - }), - hindi: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.hindi', { - defaultMessage: 'Hindi', - }), - hungarian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.hungarian', { - defaultMessage: 'Hungarian', - }), - indonesian: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.indonesian', - { - defaultMessage: 'Indonesian', - } - ), - irish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.irish', { - defaultMessage: 'Irish', - }), - italian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.italian', { - defaultMessage: 'Italian', - }), - latvian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.latvian', { - defaultMessage: 'Latvian', - }), - lithuanian: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.lithuanian', - { - defaultMessage: 'Lithuanian', - } - ), - norwegian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.norwegian', { - defaultMessage: 'Norwegian', - }), - persian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.persian', { - defaultMessage: 'Persian', - }), - portuguese: i18n.translate( - 'xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.portuguese', - { - defaultMessage: 'Portuguese', - } - ), - romanian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.romanian', { - defaultMessage: 'Romanian', - }), - russian: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.russian', { - defaultMessage: 'Russian', - }), - sorani: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.sorani', { - defaultMessage: 'Sorani', - }), - spanish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.spanish', { - defaultMessage: 'Spanish', - }), - swedish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.swedish', { - defaultMessage: 'Swedish', - }), - thai: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.thai', { - defaultMessage: 'Thai', - }), - turkish: i18n.translate('xpack.idxMgmt.mappingsEditor.formSelect.languageAnalyzer.turkish', { - defaultMessage: 'Turkish', - }), -}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts index 8addf3d9c4284..2162efd2964eb 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './default_values'; - -export * from './field_options'; - export * from './data_types_definition'; export * from './parameters_definition'; - -export * from './mappings_editor'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts deleted file mode 100644 index 1678e09512019..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/mappings_editor.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -/** - * The max nested depth allowed for child fields. - * Above this thresold, the user has to use the JSON editor. - */ -export const MAX_DEPTH_DEFAULT_EDITOR = 4; - -/** - * 16px is the default $euiSize Sass variable. - * @link https://elastic.github.io/eui/#/guidelines/sass - */ -export const EUI_SIZE = 16; - -export const CHILD_FIELD_INDENT_SIZE = EUI_SIZE * 1.5; - -export const LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER = EUI_SIZE * 0.25; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts deleted file mode 100644 index 0431ea472643b..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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. - */ - -jest.mock('../constants', () => ({ MAIN_DATA_TYPE_DEFINITION: {} })); - -import { isStateValid } from './utils'; - -describe('utils', () => { - describe('isStateValid()', () => { - let components: any; - it('handles base case', () => { - components = { - fieldsJsonEditor: { isValid: undefined }, - configuration: { isValid: undefined }, - fieldForm: undefined, - }; - expect(isStateValid(components)).toBe(undefined); - }); - - it('handles combinations of true, false and undefined', () => { - components = { - fieldsJsonEditor: { isValid: false }, - configuration: { isValid: true }, - fieldForm: undefined, - }; - - expect(isStateValid(components)).toBe(false); - - components = { - fieldsJsonEditor: { isValid: false }, - configuration: { isValid: undefined }, - fieldForm: undefined, - }; - - expect(isStateValid(components)).toBe(undefined); - - components = { - fieldsJsonEditor: { isValid: true }, - configuration: { isValid: undefined }, - fieldForm: undefined, - }; - - expect(isStateValid(components)).toBe(undefined); - - components = { - fieldsJsonEditor: { isValid: true }, - configuration: { isValid: false }, - fieldForm: undefined, - }; - - expect(isStateValid(components)).toBe(false); - - components = { - fieldsJsonEditor: { isValid: false }, - configuration: { isValid: true }, - fieldForm: { isValid: true }, - }; - - expect(isStateValid(components)).toBe(false); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts index b14a5f0d88fe9..f295d6191a5a3 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts @@ -3,31 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import uuid from 'uuid'; - -import { - DataType, - Fields, - Field, - NormalizedFields, - NormalizedField, - FieldMeta, - MainType, - SubType, - ChildFieldName, - ComboBoxOption, -} from '../types'; - -import { - SUB_TYPE_MAP_TO_MAIN, - MAX_DEPTH_DEFAULT_EDITOR, - TYPE_NOT_ALLOWED_MULTIFIELD, - TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL, -} from '../constants'; - -export const getUniqueId = () => { - return uuid.v4(); -}; +import { DataType, Field, FieldMeta, ChildFieldName } from '../types'; const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { if (dataType === 'text' || dataType === 'keyword') { @@ -64,424 +40,3 @@ export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => isExpanded: false, }; }; - -/** - * For "alias" field types, we work internaly by "id" references. When we normalize the fields, we need to - * replace the actual "path" parameter with the field (internal) `id` the alias points to. - * This method takes care of doing just that. - * - * @param byId The fields map by id - */ - -const replaceAliasPathByAliasId = ( - byId: NormalizedFields['byId'] -): { - aliases: NormalizedFields['aliases']; - byId: NormalizedFields['byId']; -} => { - const aliases: NormalizedFields['aliases'] = {}; - - Object.entries(byId).forEach(([id, field]) => { - if (field.source.type === 'alias') { - const aliasTargetField = Object.values(byId).find( - _field => _field.path.join('.') === field.source.path - ); - - if (aliasTargetField) { - // we set the path to the aliasTargetField "id" - field.source.path = aliasTargetField.id; - - // We add the alias field to our "aliases" map - aliases[aliasTargetField.id] = aliases[aliasTargetField.id] || []; - aliases[aliasTargetField.id].push(id); - } - } - }); - - return { aliases, byId }; -}; - -export const getMainTypeFromSubType = (subType: SubType): MainType => - SUB_TYPE_MAP_TO_MAIN[subType] as MainType; - -/** - * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields - * to a `byId` object where the key is the **path** to the field and the value is a `NormalizedField`. - * The `NormalizedField` contains the field data under `source` and meta information about the capability of the field. - * - * @example - -// original -{ - myObject: { - type: 'object', - properties: { - name: { - type: 'text' - } - } - } -} - -// normalized -{ - rootLevelFields: ['_uniqueId123'], - byId: { - '_uniqueId123': { - source: { type: 'object' }, - id: '_uniqueId123', - parentId: undefined, - hasChildFields: true, - childFieldsName: 'properties', // "object" type have their child fields under "properties" - canHaveChildFields: true, - childFields: ['_uniqueId456'], - }, - '_uniqueId456': { - source: { type: 'text' }, - id: '_uniqueId456', - parentId: '_uniqueId123', - hasChildFields: false, - childFieldsName: 'fields', // "text" type have their child fields under "fields" - canHaveChildFields: true, - childFields: undefined, - }, - }, -} - * - * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types) - */ -export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { - let maxNestedDepth = 0; - - const normalizeFields = ( - props: Fields, - to: NormalizedFields['byId'], - paths: string[], - arrayToKeepRef: string[], - nestedDepth: number, - isMultiField: boolean = false, - parentId?: string - ): Record => - Object.entries(props) - .sort(([a], [b]) => (a > b ? 1 : a < b ? -1 : 0)) - .reduce((acc, [propName, value]) => { - const id = getUniqueId(); - arrayToKeepRef.push(id); - const field = { name: propName, ...value } as Field; - - // In some cases for object, the "type" is not defined but the field - // has properties defined. The mappings editor requires a "type" to be defined - // so we add it here. - if (field.type === undefined && field.properties !== undefined) { - field.type = 'object'; - } - - const meta = getFieldMeta(field, isMultiField); - const { childFieldsName, hasChildFields, hasMultiFields } = meta; - - if (hasChildFields || hasMultiFields) { - const nextDepth = - meta.canHaveChildFields || meta.canHaveMultiFields ? nestedDepth + 1 : nestedDepth; - meta.childFields = []; - maxNestedDepth = Math.max(maxNestedDepth, nextDepth); - - normalizeFields( - field[childFieldsName!]!, - to, - [...paths, propName], - meta.childFields, - nextDepth, - meta.canHaveMultiFields, - id - ); - } - - const { properties, fields, ...rest } = field; - - const normalizedField: NormalizedField = { - id, - parentId, - nestedDepth, - isMultiField, - path: paths.length ? [...paths, propName] : [propName], - source: rest, - ...meta, - }; - - acc[id] = normalizedField; - - return acc; - }, to); - - const rootLevelFields: string[] = []; - const { byId, aliases } = replaceAliasPathByAliasId( - normalizeFields(fieldsToNormalize, {}, [], rootLevelFields, 0) - ); - - return { - byId, - aliases, - rootLevelFields, - maxNestedDepth, - }; -}; - -/** - * The alias "path" value internally point to a field "id" (not its path). When we deNormalize the fields, - * we need to replace the target field "id" by its actual "path", making sure to not mutate our state "fields" object. - * - * @param aliases The aliases map - * @param byId The fields map by id - */ -const replaceAliasIdByAliasPath = ( - aliases: NormalizedFields['aliases'], - byId: NormalizedFields['byId'] -): NormalizedFields['byId'] => { - const updatedById = { ...byId }; - - Object.entries(aliases).forEach(([targetId, aliasesIds]) => { - const path = updatedById[targetId] ? updatedById[targetId].path.join('.') : ''; - - aliasesIds.forEach(id => { - const aliasField = updatedById[id]; - if (!aliasField) { - return; - } - const fieldWithUpdatedPath: NormalizedField = { - ...aliasField, - source: { ...aliasField.source, path }, - }; - - updatedById[id] = fieldWithUpdatedPath; - }); - }); - - return updatedById; -}; - -export const deNormalize = ({ rootLevelFields, byId, aliases }: NormalizedFields): Fields => { - const serializedFieldsById = replaceAliasIdByAliasPath(aliases, byId); - - const deNormalizePaths = (ids: string[], to: Fields = {}) => { - ids.forEach(id => { - const { source, childFields, childFieldsName } = serializedFieldsById[id]; - const { name, ...normalizedField } = source; - const field: Omit = normalizedField; - to[name] = field; - if (childFields) { - field[childFieldsName!] = {}; - return deNormalizePaths(childFields, field[childFieldsName!]); - } - }); - return to; - }; - - return deNormalizePaths(rootLevelFields); -}; - -/** - * If we change the "name" of a field, we need to update its `path` and the - * one of **all** of its child properties or multi-fields. - * - * @param field The field who's name has changed - * @param byId The map of all the document fields - */ -export const updateFieldsPathAfterFieldNameChange = ( - field: NormalizedField, - byId: NormalizedFields['byId'] -): { updatedFieldPath: string[]; updatedById: NormalizedFields['byId'] } => { - const updatedById = { ...byId }; - const paths = field.parentId ? byId[field.parentId].path : []; - - const updateFieldPath = (_field: NormalizedField, _paths: string[]): void => { - const { name } = _field.source; - const path = _paths.length === 0 ? [name] : [..._paths, name]; - - updatedById[_field.id] = { - ..._field, - path, - }; - - if (_field.hasChildFields || _field.hasMultiFields) { - _field - .childFields!.map(fieldId => byId[fieldId]) - .forEach(childField => { - updateFieldPath(childField, [..._paths, name]); - }); - } - }; - - updateFieldPath(field, paths); - - return { updatedFieldPath: updatedById[field.id].path, updatedById }; -}; - -/** - * Retrieve recursively all the children fields of a field - * - * @param field The field to return the children from - * @param byId Map of all the document fields - */ -export const getAllChildFields = ( - field: NormalizedField, - byId: NormalizedFields['byId'] -): NormalizedField[] => { - const getChildFields = (_field: NormalizedField, to: NormalizedField[] = []) => { - if (_field.hasChildFields || _field.hasMultiFields) { - _field - .childFields!.map(fieldId => byId[fieldId]) - .forEach(childField => { - to.push(childField); - getChildFields(childField, to); - }); - } - return to; - }; - - return getChildFields(field); -}; - -/** - * If we delete an object with child fields or a text/keyword with multi-field, - * we need to know if any of its "child" fields has an `alias` that points to it. - * This method traverse the field descendant tree and returns all the aliases found - * on the field and its possible children. - */ -export const getAllDescendantAliases = ( - field: NormalizedField, - fields: NormalizedFields, - aliasesIds: string[] = [] -): string[] => { - const hasAliases = fields.aliases[field.id] && Boolean(fields.aliases[field.id].length); - - if (!hasAliases && !field.hasChildFields && !field.hasMultiFields) { - return aliasesIds; - } - - if (hasAliases) { - fields.aliases[field.id].forEach(id => { - aliasesIds.push(id); - }); - } - - if (field.childFields) { - field.childFields.forEach(id => { - if (!fields.byId[id]) { - return; - } - getAllDescendantAliases(fields.byId[id], fields, aliasesIds); - }); - } - - return aliasesIds; -}; - -/** - * Helper to retrieve a map of all the ancestors of a field - * - * @param fieldId The field id - * @param byId A map of all the fields by Id - */ -export const getFieldAncestors = ( - fieldId: string, - byId: NormalizedFields['byId'] -): { [key: string]: boolean } => { - const ancestors: { [key: string]: boolean } = {}; - const currentField = byId[fieldId]; - let parent: NormalizedField | undefined = - currentField.parentId === undefined ? undefined : byId[currentField.parentId]; - - while (parent) { - ancestors[parent.id] = true; - parent = parent.parentId === undefined ? undefined : byId[parent.parentId]; - } - - return ancestors; -}; - -export const filterTypesForMultiField = ( - options: ComboBoxOption[] -): ComboBoxOption[] => - options.filter( - option => TYPE_NOT_ALLOWED_MULTIFIELD.includes(option.value as MainType) === false - ); - -export const filterTypesForNonRootFields = ( - options: ComboBoxOption[] -): ComboBoxOption[] => - options.filter( - option => TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL.includes(option.value as MainType) === false - ); - -/** - * Return the max nested depth of the document fields - * - * @param byId Map of all the document fields - */ -export const getMaxNestedDepth = (byId: NormalizedFields['byId']): number => - Object.values(byId).reduce((maxDepth, field) => { - return Math.max(maxDepth, field.nestedDepth); - }, 0); - -/** - * Create a nested array of fields and its possible children - * to render a Tree view of them. - */ -export const buildFieldTreeFromIds = ( - fieldsIds: string[], - byId: NormalizedFields['byId'], - render: (field: NormalizedField) => JSX.Element | string -): any[] => - fieldsIds.map(id => { - const field = byId[id]; - const children = field.childFields - ? buildFieldTreeFromIds(field.childFields, byId, render) - : undefined; - - return { label: render(field), children }; - }); - -/** - * When changing the type of a field, in most cases we want to delete all its child fields. - * There are some exceptions, when changing from "text" to "keyword" as both have the same "fields" property. - */ -export const shouldDeleteChildFieldsAfterTypeChange = ( - oldType: DataType, - newType: DataType -): boolean => { - if (oldType === 'text' && newType !== 'keyword') { - return true; - } else if (oldType === 'keyword' && newType !== 'text') { - return true; - } else if (oldType === 'object' && newType !== 'nested') { - return true; - } else if (oldType === 'nested' && newType !== 'object') { - return true; - } - - return false; -}; - -export const canUseMappingsEditor = (maxNestedDepth: number) => - maxNestedDepth < MAX_DEPTH_DEFAULT_EDITOR; - -const stateWithValidity = ['configuration', 'fieldsJsonEditor', 'fieldForm']; - -export const isStateValid = (state: { [key: string]: any }): boolean | undefined => - Object.entries(state) - .filter(([key]) => stateWithValidity.includes(key)) - .reduce( - (isValid, { 1: value }) => { - if (value === undefined) { - return isValid; - } - - // If one section validity of the state is "undefined", the mappings validity is also "undefined" - if (isValid === undefined || value.isValid === undefined) { - return undefined; - } - - return isValid && value.isValid; - }, - true as undefined | boolean - ); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 5f6c2d57e35aa..9cfe29958475e 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ReactNode, OptionHTMLAttributes } from 'react'; +import { ReactNode } from 'react'; export interface DataTypeDefinition { label: string; @@ -16,19 +16,6 @@ export interface DataTypeDefinition { description?: () => ReactNode; } -export interface ParameterDefinition { - title?: string; - description?: JSX.Element | string; - fieldConfig: any; - schema?: any; - props?: { [key: string]: ParameterDefinition }; - documentation?: { - main: string; - [key: string]: string; - }; - [key: string]: any; -} - export type MainType = | 'text' | 'keyword' @@ -136,17 +123,6 @@ export type ParameterName = | 'relations' | 'max_shingle_size'; -export interface Parameter { - fieldConfig: any; - paramName?: string; - docs?: string; - props?: { [key: string]: any }; -} - -export interface Fields { - [key: string]: Omit; -} - interface FieldBasic { name: string; type: DataType; @@ -171,120 +147,8 @@ export interface FieldMeta { isExpanded: boolean; } -export interface NormalizedFields { - byId: { - [id: string]: NormalizedField; - }; - rootLevelFields: string[]; - aliases: { [key: string]: string[] }; - maxNestedDepth: number; -} - -export interface NormalizedField extends FieldMeta { - id: string; - parentId?: string; - nestedDepth: number; - path: string[]; - source: Omit; - isMultiField: boolean; -} - export type ChildFieldName = 'properties' | 'fields'; -export type FieldsEditor = 'default' | 'json'; - -export type SelectOption = { - value: unknown; - text: T | ReactNode; -} & OptionHTMLAttributes; - -export interface SuperSelectOption { - value: unknown; - inputDisplay?: ReactNode; - dropdownDisplay?: ReactNode; - disabled?: boolean; - 'data-test-subj'?: string; -} - -export interface AliasOption { - id: string; - label: string; -} - -export interface IndexSettingsInterface { - analysis?: { - analyzer: { - [key: string]: { - type: string; - tokenizer: string; - char_filter?: string[]; - filter?: string[]; - position_increment_gap?: number; - }; - }; - }; -} - -/** - * When we define the index settings we can skip - * the "index" property and directly add the "analysis". - * ES always returns the settings wrapped under "index". - */ -export type IndexSettings = IndexSettingsInterface | { index: IndexSettingsInterface }; - -export interface ComboBoxOption { - label: string; - value?: unknown; -} - -export interface SearchResult { - display: JSX.Element; - field: NormalizedField; -} - -export interface SearchMetadata { - /** - * Whether or not the search term match some part of the field path. - */ - matchPath: boolean; - /** - * If the search term matches the field type we will give it a higher score. - */ - matchType: boolean; - /** - * If the last word of the search terms matches the field name - */ - matchFieldName: boolean; - /** - * If the search term matches the beginning of the path we will give it a higher score - */ - matchStartOfPath: boolean; - /** - * If the last word of the search terms fully matches the field name - */ - fullyMatchFieldName: boolean; - /** - * If the search term exactly matches the field type - */ - fullyMatchType: boolean; - /** - * If the search term matches the full field path - */ - fullyMatchPath: boolean; - /** - * The score of the result that will allow us to sort the list - */ - score: number; - /** - * The JSX with tag wrapping the matched string - */ - display: JSX.Element; - /** - * The field path substring that matches the search - */ - stringMatch: string | null; -} - export interface GenericObject { [key: string]: any; } From 193dc95d6aeae47a09f668c2f0fccf58bc5b35c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 30 Jan 2020 11:42:30 +0530 Subject: [PATCH 08/10] Put back schemas on Parameters definition --- .../constants/parameters_definition.tsx | 139 ++++++++++-------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx index 3d380336b5eb0..2430274179e20 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import * as t from 'io-ts'; import { ParameterName } from '../types'; @@ -12,62 +13,84 @@ import { ParameterName } from '../types'; * * As a consequence, if a parameter is *not* declared here, we won't be able to declare it in the Json editor. */ -export const PARAMETERS_DEFINITION: { [key in ParameterName]: boolean } = { - name: true, - type: true, - store: true, - index: true, - doc_values: true, - doc_values_binary: true, - fielddata: true, - fielddata_frequency_filter: true, - fielddata_frequency_filter_percentage: true, - fielddata_frequency_filter_absolute: true, - coerce: true, - coerce_shape: true, - ignore_malformed: true, - null_value: true, - null_value_ip: true, - null_value_numeric: true, - null_value_boolean: true, - null_value_geo_point: true, - copy_to: true, - max_input_length: true, - locale: true, - orientation: true, - boost: true, - scaling_factor: true, - dynamic: true, - dynamic_toggle: true, - dynamic_strict: true, - enabled: true, - format: true, - analyzer: true, - search_analyzer: true, - search_quote_analyzer: true, - normalizer: true, - index_options: true, - index_options_keyword: true, - index_options_flattened: true, - eager_global_ordinals: true, - eager_global_ordinals_join: true, - index_phrases: true, - preserve_separators: true, - preserve_position_increments: true, - ignore_z_value: true, - points_only: true, - norms: true, - norms_keyword: true, - term_vector: true, - path: true, - position_increment_gap: true, - index_prefixes: true, - similarity: true, - split_queries_on_whitespace: true, - ignore_above: true, - enable_position_increments: true, - depth_limit: true, - dims: true, - relations: true, - max_shingle_size: true, +export const PARAMETERS_DEFINITION: { [key in ParameterName]: { schema?: any } } = { + name: { schema: t.string }, + type: { schema: t.string }, + store: { schema: t.boolean }, + index: { schema: t.boolean }, + doc_values: { schema: t.boolean }, + doc_values_binary: { schema: t.boolean }, + fielddata: { schema: t.boolean }, + fielddata_frequency_filter: { + schema: t.record( + t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), + t.number + ), + }, + fielddata_frequency_filter_percentage: { + schema: t.record( + t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), + t.number + ), + }, + fielddata_frequency_filter_absolute: { + schema: t.record( + t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), + t.number + ), + }, + coerce: { schema: t.boolean }, + coerce_shape: { schema: t.boolean }, + ignore_malformed: { schema: t.boolean }, + null_value: {}, + null_value_ip: {}, + null_value_numeric: { schema: t.number }, + null_value_boolean: { + schema: t.union([t.literal(true), t.literal(false), t.literal('true'), t.literal('false')]), + }, + null_value_geo_point: {}, + copy_to: { schema: t.string }, + max_input_length: { schema: t.number }, + locale: { schema: t.string }, + orientation: { schema: t.string }, + boost: { schema: t.number }, + scaling_factor: { schema: t.number }, + dynamic: { schema: t.union([t.boolean, t.literal('strict')]) }, + dynamic_toggle: {}, + dynamic_strict: {}, + enabled: { schema: t.boolean }, + format: { schema: t.string }, + analyzer: { schema: t.string }, + search_analyzer: { schema: t.string }, + search_quote_analyzer: { schema: t.string }, + normalizer: { schema: t.string }, + index_options: { schema: t.string }, + index_options_keyword: { schema: t.string }, + index_options_flattened: { schema: t.string }, + eager_global_ordinals: { schema: t.boolean }, + eager_global_ordinals_join: {}, + index_phrases: { schema: t.boolean }, + preserve_separators: { schema: t.boolean }, + preserve_position_increments: { schema: t.boolean }, + ignore_z_value: { schema: t.boolean }, + points_only: { schema: t.boolean }, + norms: { schema: t.boolean }, + norms_keyword: { schema: t.boolean }, + term_vector: { schema: t.string }, + path: { schema: t.string }, + position_increment_gap: { schema: t.number }, + index_prefixes: { + schema: t.partial({ + min_chars: t.number, + max_chars: t.number, + }), + }, + similarity: { schema: t.string }, + split_queries_on_whitespace: { schema: t.boolean }, + ignore_above: { schema: t.number }, + enable_position_increments: { schema: t.boolean }, + depth_limit: { schema: t.number }, + dims: { schema: t.string }, + relations: { schema: t.record(t.string, t.union([t.string, t.array(t.string)])) }, + max_shingle_size: { schema: t.union([t.literal(2), t.literal(3), t.literal(4)]) }, }; From f683399f9f42076e4c0ed17210feec5d55f0a4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 30 Jan 2020 12:05:01 +0530 Subject: [PATCH 09/10] Clean up unused code (2) --- .../lib/extract_mappings_definition.test.ts | 161 -------- .../lib/extract_mappings_definition.ts | 61 --- .../lib/mappings_validator.test.ts | 360 ------------------ .../mappings_editor/lib/mappings_validator.ts | 60 +-- 4 files changed, 2 insertions(+), 640 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts deleted file mode 100644 index cf399f55e660e..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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)).toEqual({ - type: 'type2', - mappings: 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)).toEqual({ - type: 'myType', - mappings: mappings.myType, - }); - }); - - test('should detect that the mappings has one custom type whose name matches a mappings definition parameter', () => { - const mappings = { - dynamic: { - _source: { - excludes: [], - includes: [], - enabled: true, - }, - _meta: {}, - _routing: { - required: false, - }, - dynamic: true, - properties: { - title: { - type: 'keyword', - }, - }, - }, - }; - - expect(extractMappingsDefinition(mappings)).toEqual({ - type: 'dynamic', - mappings: mappings.dynamic, - }); - }); - - 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)).toEqual({ mappings }); - }); -}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts index 9f2a3226b69e0..a566f466edaf9 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/extract_mappings_definition.ts @@ -54,64 +54,3 @@ const getMappingsDefinitionWithType = (mappings: GenericObject): MappingsWithTyp export const doMappingsHaveType = (mappings: GenericObject = {}): boolean => getMappingsDefinitionWithType(mappings).filter(({ type }) => type !== undefined).length > 0; - -/** - * 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 = {} -): MappingsWithType | null => { - const typedMappings = getMappingsDefinitionWithType(mappings); - - // 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; -}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts deleted file mode 100644 index d67c267dda6ae..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -/* - * 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 { validateMappings, validateProperties } from './mappings_validator'; - -describe('Mappings configuration validator', () => { - it('should convert non object to empty object', () => { - const tests = ['abc', 123, [], null, undefined]; - - tests.forEach(testValue => { - const { value, errors } = validateMappings(testValue as any); - expect(isPlainObject(value)).toBe(true); - expect(errors).toBe(undefined); - }); - }); - - it('should detect valid mappings configuration', () => { - const mappings = { - _source: { - includes: [], - excludes: [], - enabled: true, - }, - _meta: {}, - _routing: { - required: false, - }, - dynamic: true, - }; - - const { errors } = validateMappings(mappings); - expect(errors).toBe(undefined); - }); - - it('should strip out unknown configuration', () => { - const mappings = { - dynamic: true, - date_detection: true, - numeric_detection: true, - dynamic_date_formats: ['abc'], - _source: { - enabled: true, - includes: ['abc'], - excludes: ['abc'], - }, - properties: { title: { type: 'text' } }, - dynamic_templates: [], - unknown: 123, - }; - - const { value, errors } = validateMappings(mappings); - - const { unknown, ...expected } = mappings; - expect(value).toEqual(expected); - expect(errors).toEqual([{ code: 'ERR_CONFIG', configName: 'unknown' }]); - }); - - it('should strip out invalid configuration and returns the errors for each of them', () => { - const mappings = { - dynamic: true, - numeric_detection: 123, // wrong format - dynamic_date_formats: false, // wrong format - _source: { - enabled: true, - unknownProp: 'abc', // invalid - excludes: ['abc'], - }, - properties: 'abc', - }; - - const { value, errors } = validateMappings(mappings); - - expect(value).toEqual({ - dynamic: true, - properties: {}, - dynamic_templates: [], - }); - - expect(errors).not.toBe(undefined); - expect(errors!).toEqual([ - { code: 'ERR_CONFIG', configName: '_source' }, - { code: 'ERR_CONFIG', configName: 'dynamic_date_formats' }, - { code: 'ERR_CONFIG', configName: 'numeric_detection' }, - ]); - }); -}); - -describe('Properties validator', () => { - it('should convert non object to empty object', () => { - const tests = ['abc', 123, [], null, undefined]; - - tests.forEach(testValue => { - const { value, errors } = validateProperties(testValue as any); - expect(isPlainObject(value)).toBe(true); - expect(errors).toEqual([]); - }); - }); - - it('should strip non object fields', () => { - const properties = { - prop1: { type: 'text' }, - prop2: 'abc', // To be removed - prop3: 123, // To be removed - prop4: null, // To be removed - prop5: [], // To be removed - prop6: { - properties: { - prop1: { type: 'text' }, - prop2: 'abc', // To be removed - }, - }, - }; - const { value, errors } = validateProperties(properties as any); - - expect(value).toEqual({ - prop1: { type: 'text' }, - prop6: { - type: 'object', - properties: { - prop1: { type: 'text' }, - }, - }, - }); - - expect(errors).toEqual( - ['prop2', 'prop3', 'prop4', 'prop5', 'prop6.prop2'].map(fieldPath => ({ - code: 'ERR_FIELD', - fieldPath, - })) - ); - }); - - it(`should set the type to "object" when type is not provided`, () => { - const properties = { - prop1: { type: 'text' }, - prop2: {}, - prop3: { - type: 'object', - properties: { - prop1: {}, - prop2: { type: 'keyword' }, - }, - }, - }; - const { value, errors } = validateProperties(properties as any); - - expect(value).toEqual({ - prop1: { - type: 'text', - }, - prop2: { - type: 'object', - }, - prop3: { - type: 'object', - properties: { - prop1: { - type: 'object', - }, - prop2: { - type: 'keyword', - }, - }, - }, - }); - expect(errors).toEqual([]); - }); - - it('should strip field whose type is not a string or is unknown', () => { - const properties = { - prop1: { type: 123 }, - prop2: { type: 'clearlyUnknown' }, - }; - - const { value, errors } = validateProperties(properties as any); - - expect(Object.keys(value)).toEqual([]); - expect(errors).toEqual([ - { - code: 'ERR_FIELD', - fieldPath: 'prop1', - }, - { - code: 'ERR_FIELD', - fieldPath: 'prop2', - }, - ]); - }); - - it('should strip parameters that are unknown', () => { - const properties = { - prop1: { type: 'text', unknown: true, anotherUnknown: 123 }, - prop2: { type: 'keyword', store: true, index: true, doc_values_binary: true }, - prop3: { - type: 'object', - properties: { - hello: { type: 'keyword', unknown: true, anotherUnknown: 123 }, - }, - }, - }; - - const { value, errors } = validateProperties(properties as any); - - expect(value).toEqual({ - prop1: { type: 'text' }, - prop2: { type: 'keyword', store: true, index: true, doc_values_binary: true }, - prop3: { - type: 'object', - properties: { - hello: { type: 'keyword' }, - }, - }, - }); - - expect(errors).toEqual([ - { code: 'ERR_PARAMETER', fieldPath: 'prop1', paramName: 'unknown' }, - { code: 'ERR_PARAMETER', fieldPath: 'prop1', paramName: 'anotherUnknown' }, - { code: 'ERR_PARAMETER', fieldPath: 'prop3.hello', paramName: 'unknown' }, - { code: 'ERR_PARAMETER', fieldPath: 'prop3.hello', paramName: 'anotherUnknown' }, - ]); - }); - - it(`should strip parameters whose value don't have the valid type.`, () => { - const properties = { - // All the parameters in "wrongField" have a wrong format defined - // and should be stripped out when running the validation - wrongField: { - type: 'text', - store: 'abc', - index: 'abc', - doc_values: { a: 123 }, - doc_values_binary: null, - fielddata: [''], - fielddata_frequency_filter: [123, 456], - coerce: 1234, - coerce_shape: '', - ignore_malformed: 0, - null_value_numeric: 'abc', - null_value_boolean: [], - copy_to: [], - max_input_length: true, - locale: 1, - orientation: [], - boost: { a: 123 }, - scaling_factor: 'some_string', - dynamic: [true], - enabled: 'false', - format: null, - analyzer: 1, - search_analyzer: null, - search_quote_analyzer: {}, - normalizer: [], - index_options: 1, - index_options_keyword: true, - index_options_flattened: [], - eager_global_ordinals: 123, - index_phrases: null, - preserve_separators: 'abc', - preserve_position_increments: [], - ignore_z_value: {}, - points_only: [true], - norms: 'false', - norms_keyword: 'abc', - term_vector: ['no'], - path: [null], - position_increment_gap: 'abc', - index_prefixes: { min_chars: [], max_chars: 'abc' }, - similarity: 1, - split_queries_on_whitespace: {}, - ignore_above: 'abc', - enable_position_increments: [], - depth_limit: true, - dims: false, - max_shingle_size: 'string_not_allowed', - }, - // All the parameters in "goodField" have the correct format - // and should still be there after the validation ran. - goodField: { - type: 'text', - store: true, - index: true, - doc_values: true, - doc_values_binary: true, - fielddata: true, - fielddata_frequency_filter: { min: 1, max: 2, min_segment_size: 10 }, - coerce: true, - coerce_shape: true, - ignore_malformed: true, - null_value: 'NULL', - null_value_numeric: 1, - null_value_boolean: 'true', - copy_to: 'abc', - max_input_length: 10, - locale: 'en', - orientation: 'ccw', - boost: 1.5, - scaling_factor: 2.5, - dynamic: 'strict', // true | false | 'strict' are allowed - enabled: true, - format: 'strict_date_optional_time', - analyzer: 'standard', - search_analyzer: 'standard', - search_quote_analyzer: 'standard', - normalizer: 'standard', - index_options: 'positions', - index_options_keyword: 'docs', - index_options_flattened: 'docs', - eager_global_ordinals: true, - index_phrases: true, - preserve_separators: true, - preserve_position_increments: true, - ignore_z_value: true, - points_only: true, - norms: true, - norms_keyword: true, - term_vector: 'no', - path: 'abc', - position_increment_gap: 100, - index_prefixes: { min_chars: 2, max_chars: 5 }, - similarity: 'BM25', - split_queries_on_whitespace: true, - ignore_above: 64, - enable_position_increments: true, - depth_limit: 20, - dims: 'abc', - max_shingle_size: 2, - }, - goodField2: { - type: 'object', - dynamic: true, - }, - goodField3: { - type: 'object', - dynamic: false, - }, - }; - - const { value, errors } = validateProperties(properties as any); - - expect(Object.keys(value)).toEqual(['wrongField', 'goodField', 'goodField2', 'goodField3']); - - expect(value.wrongField).toEqual({ type: 'text' }); // All parameters have been stripped out but the "type". - expect(value.goodField).toEqual(properties.goodField); // All parameters are stil there. - expect(value.goodField2).toEqual(properties.goodField2); - expect(value.goodField3).toEqual(properties.goodField3); - - const allWrongParameters = Object.keys(properties.wrongField).filter(v => v !== 'type'); - expect(errors).toEqual( - allWrongParameters.map(paramName => ({ - code: 'ERR_PARAMETER', - fieldPath: 'wrongField', - paramName, - })) - ); - }); -}); diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts index fb0b7560232ee..549385df59468 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts @@ -23,17 +23,11 @@ const ALLOWED_FIELD_PROPERTIES = [ const DEFAULT_FIELD_TYPE = 'object'; -export type MappingsValidationError = +type MappingsValidationError = | { code: 'ERR_CONFIG'; configName: string } | { code: 'ERR_FIELD'; fieldPath: string } | { code: 'ERR_PARAMETER'; paramName: string; fieldPath: string }; -export interface MappingsValidatorResponse { - /* The parsed mappings object without any error */ - value: GenericObject; - errors?: MappingsValidationError[]; -} - interface PropertiesValidatorResponse { /* The parsed "properties" object without any error */ value: GenericObject; @@ -171,36 +165,11 @@ const parseFields = ( ); }; -/** - * Utility function that reads a mappings "properties" object and validate its fields by - * - Removing unknown field types - * - Removing unknown field parameters or field parameters that don't have the correct format. - * - * This method does not mutate the original properties object. It returns an object with - * the parsed properties and an array of field paths that have been removed. - * This allows us to display a warning in the UI and let the user correct the fields that we - * are about to remove. - * - * NOTE: The Joi Schema that we defined for each parameter (in "parameters_definition".tsx) - * does not do an exhaustive validation of the parameter value. - * It's main purpose is to prevent the UI from blowing up. - * - * @param properties A mappings "properties" object - */ -export const validateProperties = (properties = {}): PropertiesValidatorResponse => { - // Sanitize the input to make sure we are working with an object - if (!isPlainObject(properties)) { - return { value: {}, errors: [] }; - } - - return parseFields(properties); -}; - /** * Single source of truth to validate the *configuration* of the mappings. * Whenever a user loads a JSON object it will be validate against this Joi schema. */ -export const mappingsConfigurationSchema = t.exact( +const mappingsConfigurationSchema = t.exact( t.partial({ dynamic: t.union([t.literal(true), t.literal(false), t.literal('strict')]), date_detection: t.boolean, @@ -279,31 +248,6 @@ export const validateMappingsConfiguration = ( return { value: copyOfMappingsConfig, errors }; }; -export const validateMappings = (mappings: any = {}): MappingsValidatorResponse => { - if (!isPlainObject(mappings)) { - return { value: {} }; - } - - const { properties, dynamic_templates: dynamicTemplates, ...mappingsConfiguration } = mappings; - - const { value: parsedConfiguration, errors: configurationErrors } = validateMappingsConfiguration( - mappingsConfiguration - ); - const { value: parsedProperties, errors: propertiesErrors } = validateProperties(properties); - - const errors = [...configurationErrors, ...propertiesErrors]; - - return { - value: { - ...parsedConfiguration, - properties: parsedProperties, - dynamic_templates: - dynamicTemplates !== undefined && dynamicTemplates !== null ? dynamicTemplates : [], - }, - errors: errors.length ? errors : undefined, - }; -}; - export const VALID_MAPPINGS_PARAMETERS = [ ...mappingsConfigurationSchemaKeys, 'dynamic_templates', From fd675f84cbeebd21d32b6cbc5597a3162ba1c324 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 30 Jan 2020 21:39:13 -0800 Subject: [PATCH 10/10] Remove unused code. (#15) --- .../constants/data_types_definition.tsx | 113 ------------- .../mappings_editor/constants/index.ts | 9 -- .../constants/parameters_definition.tsx | 96 ----------- .../components/mappings_editor/lib/index.ts | 2 - .../mappings_editor/lib/mappings_validator.ts | 153 +----------------- .../components/mappings_editor/lib/utils.ts | 42 ----- .../components/mappings_editor/types.ts | 12 -- 7 files changed, 2 insertions(+), 425 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx delete mode 100644 x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx deleted file mode 100644 index 7471e85d4754b..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/data_types_definition.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { MainType, SubType, DataType, DataTypeDefinition } from '../types'; - -const TYPE_DEFINITION: { [key in DataType]: boolean } = { - text: true, - keyword: true, - numeric: true, - byte: true, - double: true, - integer: true, - long: true, - float: true, - half_float: true, - scaled_float: true, - short: true, - date: true, - date_nanos: true, - binary: true, - ip: true, - boolean: true, - range: true, - object: true, - nested: true, - rank_feature: true, - rank_features: true, - dense_vector: true, - date_range: true, - double_range: true, - float_range: true, - integer_range: true, - long_range: true, - ip_range: true, - geo_point: true, - geo_shape: true, - completion: true, - token_count: true, - percolator: true, - join: true, - alias: true, - search_as_you_type: true, - flattened: true, - shape: true, -}; - -const MAIN_TYPES: MainType[] = [ - 'alias', - 'binary', - 'boolean', - 'completion', - 'date', - 'date_nanos', - 'dense_vector', - 'flattened', - 'geo_point', - 'geo_shape', - 'ip', - 'join', - 'keyword', - 'nested', - 'numeric', - 'object', - 'percolator', - 'range', - 'rank_feature', - 'rank_features', - 'search_as_you_type', - 'shape', - 'text', - 'token_count', -]; - -const MAIN_DATA_TYPE_DEFINITION: { - [key in MainType]: DataTypeDefinition; -} = MAIN_TYPES.reduce( - (acc, type) => ({ - ...acc, - [type]: TYPE_DEFINITION[type], - }), - {} as { [key in MainType]: DataTypeDefinition } -); - -/** - * Return a map of subType -> mainType - * - * @example - * - * { - * long: 'numeric', - * integer: 'numeric', - * short: 'numeric', - * } - */ -const SUB_TYPE_MAP_TO_MAIN = Object.entries(MAIN_DATA_TYPE_DEFINITION).reduce( - (acc, [type, definition]) => { - if ({}.hasOwnProperty.call(definition, 'subTypes')) { - definition.subTypes!.types.forEach(subType => { - acc[subType] = type; - }); - } - return acc; - }, - {} as Record -); - -// Single source of truth of all the possible data types. -export const ALL_DATA_TYPES = [ - ...Object.keys(MAIN_DATA_TYPE_DEFINITION), - ...Object.keys(SUB_TYPE_MAP_TO_MAIN), -]; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts deleted file mode 100644 index 2162efd2964eb..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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. - */ - -export * from './data_types_definition'; - -export * from './parameters_definition'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx deleted file mode 100644 index 2430274179e20..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/constants/parameters_definition.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 * as t from 'io-ts'; - -import { ParameterName } from '../types'; - -/** - * Single source of truth for the parameters a user can change on _any_ field type. - * It is also the single source of truth for the parameters default values. - * - * As a consequence, if a parameter is *not* declared here, we won't be able to declare it in the Json editor. - */ -export const PARAMETERS_DEFINITION: { [key in ParameterName]: { schema?: any } } = { - name: { schema: t.string }, - type: { schema: t.string }, - store: { schema: t.boolean }, - index: { schema: t.boolean }, - doc_values: { schema: t.boolean }, - doc_values_binary: { schema: t.boolean }, - fielddata: { schema: t.boolean }, - fielddata_frequency_filter: { - schema: t.record( - t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), - t.number - ), - }, - fielddata_frequency_filter_percentage: { - schema: t.record( - t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), - t.number - ), - }, - fielddata_frequency_filter_absolute: { - schema: t.record( - t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), - t.number - ), - }, - coerce: { schema: t.boolean }, - coerce_shape: { schema: t.boolean }, - ignore_malformed: { schema: t.boolean }, - null_value: {}, - null_value_ip: {}, - null_value_numeric: { schema: t.number }, - null_value_boolean: { - schema: t.union([t.literal(true), t.literal(false), t.literal('true'), t.literal('false')]), - }, - null_value_geo_point: {}, - copy_to: { schema: t.string }, - max_input_length: { schema: t.number }, - locale: { schema: t.string }, - orientation: { schema: t.string }, - boost: { schema: t.number }, - scaling_factor: { schema: t.number }, - dynamic: { schema: t.union([t.boolean, t.literal('strict')]) }, - dynamic_toggle: {}, - dynamic_strict: {}, - enabled: { schema: t.boolean }, - format: { schema: t.string }, - analyzer: { schema: t.string }, - search_analyzer: { schema: t.string }, - search_quote_analyzer: { schema: t.string }, - normalizer: { schema: t.string }, - index_options: { schema: t.string }, - index_options_keyword: { schema: t.string }, - index_options_flattened: { schema: t.string }, - eager_global_ordinals: { schema: t.boolean }, - eager_global_ordinals_join: {}, - index_phrases: { schema: t.boolean }, - preserve_separators: { schema: t.boolean }, - preserve_position_increments: { schema: t.boolean }, - ignore_z_value: { schema: t.boolean }, - points_only: { schema: t.boolean }, - norms: { schema: t.boolean }, - norms_keyword: { schema: t.boolean }, - term_vector: { schema: t.string }, - path: { schema: t.string }, - position_increment_gap: { schema: t.number }, - index_prefixes: { - schema: t.partial({ - min_chars: t.number, - max_chars: t.number, - }), - }, - similarity: { schema: t.string }, - split_queries_on_whitespace: { schema: t.boolean }, - ignore_above: { schema: t.number }, - enable_position_increments: { schema: t.boolean }, - depth_limit: { schema: t.number }, - dims: { schema: t.string }, - relations: { schema: t.record(t.string, t.union([t.string, t.array(t.string)])) }, - max_shingle_size: { schema: t.union([t.literal(2), t.literal(3), t.literal(4)]) }, -}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts index 250795c56f322..857576440dfa4 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './utils'; - export * from './mappings_validator'; export * from './extract_mappings_definition'; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts index 549385df59468..95bbb6b07ad35 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/mappings_validator.ts @@ -3,168 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { pick, isPlainObject } from 'lodash'; +import { pick } from 'lodash'; import * as t from 'io-ts'; import { ordString } from 'fp-ts/lib/Ord'; import { toArray } from 'fp-ts/lib/Set'; -import { isLeft, isRight } from 'fp-ts/lib/Either'; +import { isLeft } from 'fp-ts/lib/Either'; import { errorReporter } from './error_reporter'; -import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants'; -import { FieldMeta } from '../types'; -import { getFieldMeta } from './utils'; - -const ALLOWED_FIELD_PROPERTIES = [ - ...Object.keys(PARAMETERS_DEFINITION), - 'type', - 'properties', - 'fields', -]; - -const DEFAULT_FIELD_TYPE = 'object'; type MappingsValidationError = | { code: 'ERR_CONFIG'; configName: string } | { code: 'ERR_FIELD'; fieldPath: string } | { code: 'ERR_PARAMETER'; paramName: string; fieldPath: string }; -interface PropertiesValidatorResponse { - /* The parsed "properties" object without any error */ - value: GenericObject; - errors: MappingsValidationError[]; -} - -interface FieldValidatorResponse { - /* The parsed field. If undefined means that it was invalid */ - value?: GenericObject; - parametersRemoved: string[]; -} - -interface GenericObject { - [key: string]: any; -} - -const validateFieldType = (type: any): boolean => { - if (typeof type !== 'string') { - return false; - } - - if (!ALL_DATA_TYPES.includes(type)) { - return false; - } - return true; -}; - -const validateParameter = (parameter: string, value: any): boolean => { - if (parameter === 'type') { - return true; - } - - if (parameter === 'name') { - return false; - } - - if (parameter === 'properties' || parameter === 'fields') { - return isPlainObject(value); - } - - const parameterSchema = (PARAMETERS_DEFINITION as any)[parameter]!.schema; - if (parameterSchema) { - return isRight(parameterSchema.decode(value)); - } - - // Fallback, if no schema defined for the parameter (this should not happen in theory) - return true; -}; - -const stripUnknownOrInvalidParameter = (field: GenericObject): FieldValidatorResponse => - Object.entries(field).reduce( - (acc, [key, value]) => { - if (!ALLOWED_FIELD_PROPERTIES.includes(key) || !validateParameter(key, value)) { - acc.parametersRemoved.push(key); - } else { - acc.value = acc.value !== undefined && acc.value !== null ? acc.value : {}; - acc.value[key] = value; - } - return acc; - }, - { parametersRemoved: [] } as FieldValidatorResponse - ); - -const parseField = (field: any): FieldValidatorResponse & { meta?: FieldMeta } => { - // Sanitize the input to make sure we are working with an object - if (!isPlainObject(field)) { - return { parametersRemoved: [] }; - } - // Make sure the field "type" is valid - if ( - !validateFieldType( - field.type !== undefined && field.type !== null ? field.type : DEFAULT_FIELD_TYPE - ) - ) { - return { parametersRemoved: [] }; - } - - // Filter out unknown or invalid "parameters" - const fieldWithType = { type: DEFAULT_FIELD_TYPE, ...field }; - const parsedField = stripUnknownOrInvalidParameter(fieldWithType); - const meta = getFieldMeta(fieldWithType); - - return { ...parsedField, meta }; -}; - -const parseFields = ( - properties: GenericObject, - path: string[] = [] -): PropertiesValidatorResponse => { - return Object.entries(properties).reduce( - (acc, [fieldName, unparsedField]) => { - const fieldPath = [...path, fieldName].join('.'); - const { value: parsedField, parametersRemoved, meta } = parseField(unparsedField); - - if (parsedField === undefined) { - // Field has been stripped out because it was invalid - acc.errors.push({ code: 'ERR_FIELD', fieldPath }); - } else { - if (meta!.hasChildFields || meta!.hasMultiFields) { - // Recursively parse all the possible children ("properties" or "fields" for multi-fields) - const parsedChildren = parseFields(parsedField[meta!.childFieldsName!], [ - ...path, - fieldName, - ]); - parsedField[meta!.childFieldsName!] = parsedChildren.value; - - /** - * If the children parsed have any error we concatenate them in our accumulator. - */ - if (parsedChildren.errors) { - acc.errors = [...acc.errors, ...parsedChildren.errors]; - } - } - - acc.value[fieldName] = parsedField; - - if (Boolean(parametersRemoved.length)) { - acc.errors = [ - ...acc.errors, - ...parametersRemoved.map(paramName => ({ - code: 'ERR_PARAMETER' as 'ERR_PARAMETER', - fieldPath, - paramName, - })), - ]; - } - } - - return acc; - }, - { - value: {}, - errors: [], - } as PropertiesValidatorResponse - ); -}; - /** * Single source of truth to validate the *configuration* of the mappings. * Whenever a user loads a JSON object it will be validate against this Joi schema. diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts deleted file mode 100644 index f295d6191a5a3..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/lib/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DataType, Field, FieldMeta, ChildFieldName } from '../types'; - -const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { - if (dataType === 'text' || dataType === 'keyword') { - return 'fields'; - } else if (dataType === 'object' || dataType === 'nested') { - return 'properties'; - } - return undefined; -}; - -export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => { - const childFieldsName = getChildFieldsName(field.type); - - const canHaveChildFields = isMultiField ? false : childFieldsName === 'properties'; - const hasChildFields = isMultiField - ? false - : canHaveChildFields && - Boolean(field[childFieldsName!]) && - Object.keys(field[childFieldsName!]!).length > 0; - - const canHaveMultiFields = isMultiField ? false : childFieldsName === 'fields'; - const hasMultiFields = isMultiField - ? false - : canHaveMultiFields && - Boolean(field[childFieldsName!]) && - Object.keys(field[childFieldsName!]!).length > 0; - - return { - childFieldsName, - canHaveChildFields, - hasChildFields, - canHaveMultiFields, - hasMultiFields, - isExpanded: false, - }; -}; diff --git a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts index 9cfe29958475e..a4a6fef10ee6c 100644 --- a/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/components/mappings_editor/types.ts @@ -137,18 +137,6 @@ type FieldParams = { export type Field = FieldBasic & Partial; -export interface FieldMeta { - childFieldsName: ChildFieldName | undefined; - canHaveChildFields: boolean; - canHaveMultiFields: boolean; - hasChildFields: boolean; - hasMultiFields: boolean; - childFields?: string[]; - isExpanded: boolean; -} - -export type ChildFieldName = 'properties' | 'fields'; - export interface GenericObject { [key: string]: any; }