diff --git a/api_docs/index_pattern_field_editor.json b/api_docs/index_pattern_field_editor.json
index 25cb2cb1d6ea9..e35d8685c4059 100644
--- a/api_docs/index_pattern_field_editor.json
+++ b/api_docs/index_pattern_field_editor.json
@@ -26,7 +26,13 @@
"text": "FormatEditorProps"
},
"
, ",
- "FormatEditorState",
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.FormatEditorState",
+ "text": "FormatEditorState"
+ },
" & S, any>"
],
"path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx",
@@ -50,7 +56,13 @@
"label": "state",
"description": [],
"signature": [
- "FormatEditorState",
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.FormatEditorState",
+ "text": "FormatEditorState"
+ },
" & S"
],
"path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx",
@@ -73,9 +85,21 @@
"text": "FormatEditorProps"
},
"<{}>, state: ",
- "FormatEditorState",
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.FormatEditorState",
+ "text": "FormatEditorState"
+ },
") => { error: string | undefined; samples: ",
- "Sample",
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.Sample",
+ "text": "Sample"
+ },
"[]; }"
],
"path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx",
@@ -110,7 +134,13 @@
"label": "state",
"description": [],
"signature": [
- "FormatEditorState"
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.FormatEditorState",
+ "text": "FormatEditorState"
+ }
],
"path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx",
"deprecated": false,
@@ -307,6 +337,53 @@
],
"initialIsOpen": false
},
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.FormatEditorState",
+ "type": "Interface",
+ "tags": [],
+ "label": "FormatEditorState",
+ "description": [],
+ "path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx",
+ "deprecated": false,
+ "children": [
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.FormatEditorState.EditorComponent",
+ "type": "CompoundType",
+ "tags": [],
+ "label": "EditorComponent",
+ "description": [],
+ "signature": [
+ "React.LazyExoticComponent<",
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.FieldFormatEditor",
+ "text": "FieldFormatEditor"
+ },
+ "<{}>> | null"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx",
+ "deprecated": false
+ },
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.FormatEditorState.fieldFormatId",
+ "type": "string",
+ "tags": [],
+ "label": "fieldFormatId",
+ "description": [],
+ "signature": [
+ "string | undefined"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx",
+ "deprecated": false
+ }
+ ],
+ "initialIsOpen": false
+ },
{
"parentPluginId": "indexPatternFieldEditor",
"id": "def-public.OpenFieldDeleteModalOptions",
@@ -431,7 +508,7 @@
"section": "def-common.IndexPatternField",
"text": "IndexPatternField"
},
- ") => void) | undefined"
+ "[]) => void) | undefined"
],
"path": "src/plugins/index_pattern_field_editor/public/open_editor.tsx",
"deprecated": false,
@@ -439,7 +516,7 @@
{
"parentPluginId": "indexPatternFieldEditor",
"id": "def-public.OpenFieldEditorOptions.onSave.$1",
- "type": "Object",
+ "type": "Array",
"tags": [],
"label": "field",
"description": [],
@@ -450,7 +527,8 @@
"docId": "kibDataIndexPatternsPluginApi",
"section": "def-common.IndexPatternField",
"text": "IndexPatternField"
- }
+ },
+ "[]"
],
"path": "src/plugins/index_pattern_field_editor/public/open_editor.tsx",
"deprecated": false,
@@ -474,6 +552,134 @@
}
],
"initialIsOpen": false
+ },
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Props",
+ "type": "Interface",
+ "tags": [],
+ "label": "Props",
+ "description": [],
+ "path": "src/plugins/index_pattern_field_editor/public/components/delete_field_provider.tsx",
+ "deprecated": false,
+ "children": [
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Props.children",
+ "type": "Function",
+ "tags": [],
+ "label": "children",
+ "description": [],
+ "signature": [
+ "(deleteFieldHandler: DeleteFieldFunc) => React.ReactNode"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/delete_field_provider.tsx",
+ "deprecated": false,
+ "children": [
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Props.children.$1",
+ "type": "Function",
+ "tags": [],
+ "label": "deleteFieldHandler",
+ "description": [],
+ "signature": [
+ "DeleteFieldFunc"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/delete_field_provider.tsx",
+ "deprecated": false,
+ "isRequired": true
+ }
+ ],
+ "returnComment": []
+ },
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Props.indexPattern",
+ "type": "Object",
+ "tags": [],
+ "label": "indexPattern",
+ "description": [],
+ "signature": [
+ {
+ "pluginId": "data",
+ "scope": "common",
+ "docId": "kibDataIndexPatternsPluginApi",
+ "section": "def-common.IndexPattern",
+ "text": "IndexPattern"
+ }
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/delete_field_provider.tsx",
+ "deprecated": false
+ },
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Props.onDelete",
+ "type": "Function",
+ "tags": [],
+ "label": "onDelete",
+ "description": [],
+ "signature": [
+ "((fieldNames: string[]) => void) | undefined"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/delete_field_provider.tsx",
+ "deprecated": false,
+ "children": [
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Props.onDelete.$1",
+ "type": "Array",
+ "tags": [],
+ "label": "fieldNames",
+ "description": [],
+ "signature": [
+ "string[]"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/delete_field_provider.tsx",
+ "deprecated": false,
+ "isRequired": true
+ }
+ ],
+ "returnComment": []
+ }
+ ],
+ "initialIsOpen": false
+ },
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Sample",
+ "type": "Interface",
+ "tags": [],
+ "label": "Sample",
+ "description": [],
+ "path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts",
+ "deprecated": false,
+ "children": [
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Sample.input",
+ "type": "CompoundType",
+ "tags": [],
+ "label": "input",
+ "description": [],
+ "signature": [
+ "string | number | React.ReactText[] | Record"
+ ],
+ "path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts",
+ "deprecated": false
+ },
+ {
+ "parentPluginId": "indexPatternFieldEditor",
+ "id": "def-public.Sample.output",
+ "type": "string",
+ "tags": [],
+ "label": "output",
+ "description": [],
+ "path": "src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts",
+ "deprecated": false
+ }
+ ],
+ "initialIsOpen": false
}
],
"enums": [],
@@ -722,7 +928,13 @@
"description": [],
"signature": [
"React.FunctionComponent<",
- "Props",
+ {
+ "pluginId": "indexPatternFieldEditor",
+ "scope": "public",
+ "docId": "kibIndexPatternFieldEditorPluginApi",
+ "section": "def-public.Props",
+ "text": "Props"
+ },
">"
],
"path": "src/plugins/index_pattern_field_editor/public/types.ts",
diff --git a/api_docs/index_pattern_field_editor.mdx b/api_docs/index_pattern_field_editor.mdx
index 7a3cfd0e66bbe..bdf963a71433d 100644
--- a/api_docs/index_pattern_field_editor.mdx
+++ b/api_docs/index_pattern_field_editor.mdx
@@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services
| Public API count | Any count | Items lacking comments | Missing exports |
|-------------------|-----------|------------------------|-----------------|
-| 42 | 2 | 39 | 3 |
+| 54 | 2 | 51 | 0 |
## Client
diff --git a/src/plugins/data/common/index_patterns/constants.ts b/src/plugins/data/common/index_patterns/constants.ts
index 67e266dbd84a2..43e41832e1770 100644
--- a/src/plugins/data/common/index_patterns/constants.ts
+++ b/src/plugins/data/common/index_patterns/constants.ts
@@ -14,6 +14,7 @@ export const RUNTIME_FIELD_TYPES = [
'ip',
'boolean',
'geo_point',
+ 'composite',
] as const;
/**
diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts
index 78cc390c8c13f..48e641ce68d50 100644
--- a/src/plugins/data/common/index_patterns/fields/field_list.ts
+++ b/src/plugins/data/common/index_patterns/fields/field_list.ts
@@ -15,7 +15,7 @@ import { DataView } from '../index_patterns';
type FieldMap = Map;
export interface IIndexPatternFieldList extends Array {
- add(field: FieldSpec): void;
+ add(field: FieldSpec): DataViewField;
getAll(): DataViewField[];
getByName(name: DataViewField['name']): DataViewField | undefined;
getByType(type: DataViewField['type']): DataViewField[];
@@ -55,11 +55,12 @@ export const fieldList = (
public readonly getByType = (type: DataViewField['type']) => [
...(this.groups.get(type) || new Map()).values(),
];
- public readonly add = (field: FieldSpec) => {
+ public readonly add = (field: FieldSpec): DataViewField => {
const newField = new DataViewField({ ...field, shortDotsEnable });
this.push(newField);
this.setByName(newField);
this.setByGroup(newField);
+ return newField;
};
public readonly remove = (field: IFieldType) => {
diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
index fae0e14b95c05..1f0e8cf076ebc 100644
--- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
+++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts
@@ -9,7 +9,7 @@
/* eslint-disable max-classes-per-file */
import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '@kbn/field-types';
-import type { RuntimeField } from '../types';
+import type { RuntimeFieldSpec, RuntimeType } from '../types';
import { KBN_FIELD_TYPES } from '../../kbn_field_types/types';
import type { IFieldType } from './types';
import { FieldSpec, DataView } from '../..';
@@ -43,7 +43,7 @@ export class DataViewField implements IFieldType {
return this.spec.runtimeField;
}
- public set runtimeField(runtimeField: RuntimeField | undefined) {
+ public set runtimeField(runtimeField: RuntimeFieldSpec | undefined) {
this.spec.runtimeField = runtimeField;
}
@@ -102,13 +102,29 @@ export class DataViewField implements IFieldType {
}
public get type() {
- return this.runtimeField?.type
- ? castEsToKbnFieldTypeName(this.runtimeField?.type)
- : this.spec.type;
+ if (this.isRuntimeField) {
+ let type: RuntimeType = this.runtimeField?.type!;
+ if (this.isRuntimeCompositeSubField) {
+ const [, subFieldName] = this.name.split('.');
+ // We return the subField type (with fallback mechanism to "composite" type)
+ type = this.runtimeField?.fields![subFieldName]?.type ?? this.runtimeField?.type!;
+ }
+ return castEsToKbnFieldTypeName(type);
+ }
+
+ return this.spec.type;
}
public get esTypes() {
- return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes;
+ if (this.isRuntimeField) {
+ if (this.isRuntimeCompositeSubField) {
+ const [, subFieldName] = this.name.split('.');
+ // We return the subField type (with fallback mechanism to "composite" type)
+ return [this.runtimeField?.fields![subFieldName]?.type ?? this.runtimeField?.type!];
+ }
+ return [this.runtimeField?.type!];
+ }
+ return this.spec.esTypes;
}
public get scripted() {
@@ -138,6 +154,10 @@ export class DataViewField implements IFieldType {
return this.spec.isMapped;
}
+ public get isRuntimeField() {
+ return !this.isMapped && this.runtimeField !== undefined;
+ }
+
// not writable, not serialized
public get sortable() {
return (
@@ -206,6 +226,13 @@ export class DataViewField implements IFieldType {
isMapped: this.isMapped,
};
}
+
+ private get isRuntimeCompositeSubField(): boolean {
+ if (!this.isRuntimeField) {
+ return false;
+ }
+ return this.runtimeField!.type === 'composite';
+ }
}
/**
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index e08d1e62bae06..52a0833649206 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -11,7 +11,7 @@
import _, { each, reject } from 'lodash';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '../..';
-import type { RuntimeField } from '../types';
+import type { RuntimeField, RuntimeFieldSpec, RuntimeType, FieldConfiguration } from '../types';
import { DuplicateField } from '../../../../kibana_utils/common';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common';
@@ -79,7 +79,7 @@ export class DataView implements IIndexPattern {
private shortDotsEnable: boolean = false;
private fieldFormats: FieldFormatsStartCommon;
private fieldAttrs: FieldAttrs;
- private runtimeFieldMap: Record;
+ private runtimeFieldMap: Record;
/**
* prevents errors when index pattern exists before indices
@@ -192,11 +192,13 @@ export class DataView implements IIndexPattern {
};
});
+ const runtimeFields = this.getRuntimeMappings();
+
return {
storedFields: ['*'],
scriptFields,
docvalueFields,
- runtimeFields: this.runtimeFieldMap,
+ runtimeFields,
};
}
@@ -360,26 +362,32 @@ export class DataView implements IIndexPattern {
/**
* Add a runtime field - Appended to existing mapped field or a new field is
- * created as appropriate
+ * created as appropriate.
* @param name Field name
* @param runtimeField Runtime field definition
*/
- addRuntimeField(name: string, runtimeField: RuntimeField) {
- const existingField = this.getFieldByName(name);
- if (existingField) {
- existingField.runtimeField = runtimeField;
- } else {
- this.fields.add({
- name,
- runtimeField,
- type: castEsToKbnFieldTypeName(runtimeField.type),
- aggregatable: true,
- searchable: true,
- count: 0,
- readFromDocValues: false,
- });
+ addRuntimeField(name: string, runtimeField: RuntimeField): DataViewField[] {
+ const { type, script, customLabel, format, popularity } = runtimeField;
+
+ if (type === 'composite') {
+ return this.addCompositeRuntimeField(name, runtimeField);
}
- this.runtimeFieldMap[name] = runtimeField;
+
+ const runtimeFieldSpec: RuntimeFieldSpec = {
+ type,
+ script,
+ };
+
+ const dataViewFields = [
+ this.updateOrAddRuntimeField(name, type, runtimeFieldSpec, {
+ customLabel,
+ format,
+ popularity,
+ }),
+ ];
+
+ this.runtimeFieldMap[name] = runtimeFieldSpec;
+ return dataViewFields;
}
/**
@@ -395,7 +403,60 @@ export class DataView implements IIndexPattern {
* @param name
*/
getRuntimeField(name: string): RuntimeField | null {
- return this.runtimeFieldMap[name] ?? null;
+ if (!this.runtimeFieldMap[name]) {
+ return null;
+ }
+
+ const { type, script, fields } = { ...this.runtimeFieldMap[name] };
+ const runtimeField: RuntimeField = {
+ type,
+ script,
+ };
+
+ if (type === 'composite') {
+ const subFields = Object.entries(fields!).reduce(
+ (acc, [subFieldName, subField]) => {
+ const fieldFullName = `${name}.${subFieldName}`;
+ const dataViewField = this.getFieldByName(fieldFullName);
+ if (!dataViewField) {
+ // We should never enter here as all composite runtime subfield
+ // are converted to data view fields.
+ return acc;
+ }
+ return {
+ ...acc,
+ [subFieldName]: {
+ type: subField.type,
+ format: this.getFormatterForFieldNoDefault(fieldFullName)?.toJSON(),
+ customLabel: dataViewField.customLabel,
+ popularity: dataViewField.count,
+ },
+ };
+ },
+ {}
+ );
+
+ runtimeField.fields = subFields;
+ } else {
+ const dataViewField = this.getFieldByName(name);
+ if (dataViewField) {
+ runtimeField.customLabel = dataViewField.customLabel;
+ runtimeField.popularity = dataViewField.count;
+ runtimeField.format = this.getFormatterForFieldNoDefault(name)?.toJSON();
+ }
+ }
+
+ return runtimeField;
+ }
+
+ getAllRuntimeFields(): Record {
+ return Object.keys(this.runtimeFieldMap).reduce>(
+ (acc, fieldName) => ({
+ ...acc,
+ [fieldName]: this.getRuntimeField(fieldName)!,
+ }),
+ {}
+ );
}
/**
@@ -420,6 +481,7 @@ export class DataView implements IIndexPattern {
*/
removeRuntimeField(name: string) {
const existingField = this.getFieldByName(name);
+
if (existingField) {
if (existingField.isMapped) {
// mapped field, remove runtimeField def
@@ -427,10 +489,30 @@ export class DataView implements IIndexPattern {
} else {
this.fields.remove(existingField);
}
+ } else {
+ const runtimeFieldSpec = this.runtimeFieldMap[name];
+
+ if (runtimeFieldSpec?.type === 'composite') {
+ // If we remove a "composite" runtime field we loop through each of its
+ // subFields and remove them from the field list
+ Object.keys(runtimeFieldSpec.fields!).forEach((subFieldName) => {
+ const subField = this.getFieldByName(`${name}.${subFieldName}`);
+ if (subField) {
+ this.fields.remove(subField);
+ }
+ });
+ }
}
delete this.runtimeFieldMap[name];
}
+ /**
+ * Return the "runtime_mappings" section of the ES search query
+ */
+ getRuntimeMappings(): Record {
+ return _.cloneDeep(this.runtimeFieldMap);
+ }
+
/**
* Get formatter for a given field name. Return undefined if none exists
* @param field
@@ -484,6 +566,92 @@ export class DataView implements IIndexPattern {
public readonly deleteFieldFormat = (fieldName: string) => {
delete this.fieldFormatMap[fieldName];
};
+
+ private addCompositeRuntimeField(name: string, runtimeField: RuntimeField): DataViewField[] {
+ const { type, script, fields } = runtimeField;
+
+ // Make sure subFields are provided
+ if (fields === undefined || Object.keys(fields).length === 0) {
+ throw new Error(`Can't add composite runtime field [name = ${name}] without subfields.`);
+ }
+
+ // Make sure no field with the same name already exist
+ if (this.getFieldByName(name) !== undefined) {
+ throw new Error(
+ `Can't create composite runtime field ["${name}"] as there is already a field with this name`
+ );
+ }
+
+ const runtimeFieldSpecFields: RuntimeFieldSpec['fields'] = Object.entries(fields).reduce<
+ RuntimeFieldSpec['fields']
+ >((acc, [subFieldName, subField]) => {
+ return {
+ ...acc,
+ [subFieldName]: {
+ type: subField.type,
+ },
+ };
+ }, {});
+
+ const runtimeFieldSpec: RuntimeFieldSpec = {
+ type,
+ script,
+ fields: runtimeFieldSpecFields,
+ };
+
+ // We first remove the runtime composite field with the same name which will remove all of its subFields.
+ // This guarantees that we don't leave behind orphan data view fields
+ this.removeRuntimeField(name);
+
+ // We don't add composite runtime fields to the field list as
+ // they are not fields but **holder** of fields.
+ // What we do add to the field list are all their subFields.
+ const dataViewFields = Object.entries(fields).map(([subFieldName, subField]) =>
+ this.updateOrAddRuntimeField(`${name}.${subFieldName}`, subField.type, runtimeFieldSpec, {
+ customLabel: subField.customLabel,
+ format: subField.format,
+ popularity: subField.popularity,
+ })
+ );
+
+ this.runtimeFieldMap[name] = runtimeFieldSpec;
+ return dataViewFields;
+ }
+
+ private updateOrAddRuntimeField(
+ fieldName: string,
+ fieldType: RuntimeType,
+ runtimeFieldSpec: RuntimeFieldSpec,
+ config: FieldConfiguration
+ ): DataViewField {
+ // Create the field if it does not exist or update an existing one
+ let createdField: DataViewField | undefined;
+ const existingField = this.getFieldByName(fieldName);
+
+ if (existingField) {
+ existingField.runtimeField = runtimeFieldSpec;
+ } else {
+ createdField = this.fields.add({
+ name: fieldName,
+ runtimeField: runtimeFieldSpec,
+ type: castEsToKbnFieldTypeName(fieldType),
+ aggregatable: true,
+ searchable: true,
+ count: config.popularity ?? 0,
+ readFromDocValues: false,
+ });
+ }
+
+ // Apply configuration to the field
+ this.setFieldCustomLabel(fieldName, config.customLabel);
+ if (config.format) {
+ this.setFieldFormat(fieldName, config.format);
+ } else if (config.format === null) {
+ this.deleteFieldFormat(fieldName);
+ }
+
+ return createdField ?? existingField!;
+ }
}
/**
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
index a72224d1c3fe8..3201c443ebe3e 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
@@ -14,7 +14,7 @@ import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { DATA_VIEW_SAVED_OBJECT_TYPE, SavedObjectsClientCommon } from '../..';
import { createDataViewCache } from '.';
-import type { RuntimeField } from '../types';
+import type { RuntimeField, RuntimeFieldSpec, RuntimeType } from '../types';
import { DataView } from './index_pattern';
import { createEnsureDefaultDataView, EnsureDefaultDataView } from './ensure_default_index_pattern';
import {
@@ -456,20 +456,35 @@ export class DataViewsService {
spec.fieldAttrs
);
+ const addRuntimeFieldToSpecFields = (
+ name: string,
+ fieldType: RuntimeType,
+ runtimeField: RuntimeFieldSpec
+ ) => {
+ spec.fields![name] = {
+ name,
+ type: castEsToKbnFieldTypeName(fieldType),
+ runtimeField,
+ aggregatable: true,
+ searchable: true,
+ readFromDocValues: false,
+ customLabel: spec.fieldAttrs?.[name]?.customLabel,
+ count: spec.fieldAttrs?.[name]?.count,
+ };
+ };
+
// CREATE RUNTIME FIELDS
- for (const [key, value] of Object.entries(runtimeFieldMap || {})) {
+ for (const [name, runtimeField] of Object.entries(runtimeFieldMap || {})) {
// do not create runtime field if mapped field exists
- if (!spec.fields[key]) {
- spec.fields[key] = {
- name: key,
- type: castEsToKbnFieldTypeName(value.type),
- runtimeField: value,
- aggregatable: true,
- searchable: true,
- readFromDocValues: false,
- customLabel: spec.fieldAttrs?.[key]?.customLabel,
- count: spec.fieldAttrs?.[key]?.count,
- };
+ if (!spec.fields[name]) {
+ // For composite runtime field we add the subFields, **not** the composite
+ if (runtimeField.type === 'composite') {
+ Object.entries(runtimeField.fields!).forEach(([subFieldName, subField]) => {
+ addRuntimeFieldToSpecFields(`${name}.${subFieldName}`, subField.type, runtimeField);
+ });
+ } else {
+ addRuntimeFieldToSpecFields(name, runtimeField.type, runtimeField);
+ }
}
}
} catch (err) {
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index d1e822aea4e97..ce6853828c8f8 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -19,13 +19,49 @@ import { FieldFormat } from '../../../field_formats/common';
export type FieldFormatMap = Record;
export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
-export interface RuntimeField {
+
+export type RuntimeTypeExceptComposite = Exclude;
+
+export interface RuntimeFieldBase {
type: RuntimeType;
script?: {
source: string;
};
}
+/**
+ * The RuntimeField that will be sent in the ES Query "runtime_mappings" object
+ */
+export interface RuntimeFieldSpec extends RuntimeFieldBase {
+ fields?: Record<
+ string,
+ {
+ // It is not recursive, we can't create a composite inside a composite.
+ type: RuntimeTypeExceptComposite;
+ }
+ >;
+}
+
+export interface FieldConfiguration {
+ format?: SerializedFieldFormat | null;
+ customLabel?: string;
+ popularity?: number;
+}
+
+/**
+ * This is the RuntimeField interface enhanced with Data view field
+ * configuration: field format definition, customLabel or popularity.
+ *
+ * @see {@link RuntimeField}
+ */
+export interface RuntimeField extends RuntimeFieldBase, FieldConfiguration {
+ fields?: Record;
+}
+
+export interface RuntimeFieldSubField extends FieldConfiguration {
+ type: RuntimeTypeExceptComposite;
+}
+
/**
* @deprecated
* IIndexPattern allows for an IndexPattern OR an index pattern saved object
@@ -218,7 +254,7 @@ export interface FieldSpec extends DataViewFieldBase {
readFromDocValues?: boolean;
indexed?: boolean;
customLabel?: string;
- runtimeField?: RuntimeField;
+ runtimeField?: RuntimeFieldSpec;
// not persisted
shortDotsEnable?: boolean;
isMapped?: boolean;
@@ -256,7 +292,7 @@ export interface DataViewSpec {
typeMeta?: TypeMeta;
type?: string;
fieldFormats?: Record;
- runtimeFieldMap?: Record;
+ runtimeFieldMap?: Record;
fieldAttrs?: FieldAttrs;
allowNoIndex?: boolean;
}
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index 595a88b412e9f..80741cf3993e3 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -89,6 +89,7 @@ export {
AggregationRestrictions,
IndexPatternType,
IndexPatternListItem,
+ RuntimeType,
} from '../common';
export { DuplicateDataViewError } from '../common/index_patterns/errors';
diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts
index faf6d87b6d10b..9854e5f79c226 100644
--- a/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts
+++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts
@@ -7,6 +7,7 @@
*/
import { schema } from '@kbn/config-schema';
+import { RuntimeField } from '../../../../common';
import { handleErrors } from '../util/handle_errors';
import { runtimeFieldSpecSchema } from '../util/schemas';
import { IRouter, StartServicesAccessor } from '../../../../../../core/server';
@@ -53,19 +54,18 @@ export const registerCreateRuntimeFieldRoute = (
throw new Error(`Field [name = ${name}] already exists.`);
}
- indexPattern.addRuntimeField(name, runtimeField);
-
- const addedField = indexPattern.fields.getByName(name);
- if (!addedField) throw new Error(`Could not create a field [name = ${name}].`);
+ const createdFields = indexPattern.addRuntimeField(name, runtimeField as RuntimeField);
await indexPatternsService.updateSavedObject(indexPattern);
- const savedField = indexPattern.fields.getByName(name);
- if (!savedField) throw new Error(`Could not create a field [name = ${name}].`);
-
return res.ok({
body: {
- field: savedField.toSpec(),
+ // New API for 7.16 & 8.x. Return an Array of DataViewFields created
+ fields: createdFields.map((f) => f.toSpec()),
+ // @deprecated
+ // To avoid creating a breaking change in 7.16 we continue to support
+ // the old "field" in the response
+ field: createdFields[0].toSpec(),
index_pattern: indexPattern.toSpec(),
},
});
diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts
index 58b8529d7cf5a..b2c75e14a3a8b 100644
--- a/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts
+++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts
@@ -44,16 +44,12 @@ export const registerDeleteRuntimeFieldRoute = (
const name = req.params.name;
const indexPattern = await indexPatternsService.get(id);
- const field = indexPattern.fields.getByName(name);
+ const runtimeField = indexPattern.getRuntimeField(name);
- if (!field) {
+ if (!runtimeField) {
throw new ErrorIndexPatternFieldNotFound(id, name);
}
- if (!field.runtimeField) {
- throw new Error('Only runtime fields can be deleted.');
- }
-
indexPattern.removeRuntimeField(name);
await indexPatternsService.updateSavedObject(indexPattern);
diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts
index 6bc2bf396c0b4..f5b2b29648313 100644
--- a/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts
+++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts
@@ -7,6 +7,7 @@
*/
import { schema } from '@kbn/config-schema';
+import { DataViewField } from '../../../../common';
import { ErrorIndexPatternFieldNotFound } from '../../error';
import { handleErrors } from '../util/handle_errors';
import { IRouter, StartServicesAccessor } from '../../../../../../core/server';
@@ -46,20 +47,36 @@ export const registerGetRuntimeFieldRoute = (
const indexPattern = await indexPatternsService.get(id);
- const field = indexPattern.fields.getByName(name);
+ const runtimeField = indexPattern.getRuntimeField(name);
- if (!field) {
+ if (!runtimeField) {
throw new ErrorIndexPatternFieldNotFound(id, name);
}
- if (!field.runtimeField) {
- throw new Error('Only runtime fields can be retrieved.');
+ // Access the data view fields created for the runtime field
+ let dataViewFields: DataViewField[];
+
+ if (runtimeField.type === 'composite') {
+ // For "composite" runtime fields we need to look at the "fields"
+ dataViewFields = Object.keys(runtimeField.fields!)
+ .map((subFieldName) => {
+ const fullName = `${name}.${subFieldName}`;
+ return indexPattern.fields.getByName(fullName);
+ })
+ .filter(Boolean) as DataViewField[];
+ } else {
+ dataViewFields = [indexPattern.fields.getByName(name)].filter(Boolean) as DataViewField[];
}
return res.ok({
body: {
- field: field.toSpec(),
- runtimeField: indexPattern.getRuntimeField(name),
+ // New API for 7.16 & 8.x. Return an Array of DataViewFields for the runtime field
+ fields: dataViewFields,
+ // @deprecated
+ // To avoid creating a breaking change in 7.16 we continue to support
+ // the old "field" in the response
+ field: dataViewFields[0].toSpec(),
+ runtimeField,
},
});
})
diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts
index a5e92fa5a36ec..1397c25d12068 100644
--- a/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts
+++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts
@@ -7,6 +7,7 @@
*/
import { schema } from '@kbn/config-schema';
+import { RuntimeField } from 'src/plugins/data/common';
import { handleErrors } from '../util/handle_errors';
import { runtimeFieldSpecSchema } from '../util/schemas';
import { IRouter, StartServicesAccessor } from '../../../../../../core/server';
@@ -44,30 +45,31 @@ export const registerPutRuntimeFieldRoute = (
elasticsearchClient
);
const id = req.params.id;
- const { name, runtimeField } = req.body;
+ const { name, runtimeField } = req.body as {
+ name: string;
+ runtimeField: RuntimeField;
+ };
const indexPattern = await indexPatternsService.get(id);
- const oldFieldObject = indexPattern.fields.getByName(name);
+ const oldRuntimeFieldObject = indexPattern.getRuntimeField(name);
- if (oldFieldObject && !oldFieldObject.runtimeField) {
- throw new Error('Only runtime fields can be updated');
- }
-
- if (oldFieldObject) {
+ if (oldRuntimeFieldObject) {
indexPattern.removeRuntimeField(name);
}
- indexPattern.addRuntimeField(name, runtimeField);
+ const createdFields = indexPattern.addRuntimeField(name, runtimeField);
await indexPatternsService.updateSavedObject(indexPattern);
- const fieldObject = indexPattern.fields.getByName(name);
- if (!fieldObject) throw new Error(`Could not create a field [name = ${name}].`);
-
return res.ok({
body: {
- field: fieldObject.toSpec(),
+ // New API for 7.16 & 8.x. Return an Array of DataViewFields created
+ fields: createdFields.map((f) => f.toSpec()),
+ // @deprecated
+ // To avoid creating a breaking change in 7.16 we continue to support
+ // the old "field" in the response
+ field: createdFields[0].toSpec(),
index_pattern: indexPattern.toSpec(),
},
});
diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts
index 3f3aae46c4388..dd2a2fee91049 100644
--- a/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts
+++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts
@@ -63,19 +63,21 @@ export const registerUpdateRuntimeFieldRoute = (
}
indexPattern.removeRuntimeField(name);
- indexPattern.addRuntimeField(name, {
+ const createdFields = indexPattern.addRuntimeField(name, {
...existingRuntimeField,
...runtimeField,
});
await indexPatternsService.updateSavedObject(indexPattern);
- const fieldObject = indexPattern.fields.getByName(name);
- if (!fieldObject) throw new Error(`Could not create a field [name = ${name}].`);
-
return res.ok({
body: {
- field: fieldObject.toSpec(),
+ // New API for 7.16 & 8.x. Return an Array of DataViewFields created
+ fields: createdFields.map((f) => f.toSpec()),
+ // @deprecated
+ // To avoid creating a breaking change in 7.16 we continue to support
+ // the old "field" in the response
+ field: createdFields[0].toSpec(),
index_pattern: indexPattern.toSpec(),
},
});
diff --git a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts
index 1c88550c154c5..8936d45ba4291 100644
--- a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts
+++ b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts
@@ -14,6 +14,7 @@ import {
serializedFieldFormatSchema,
} from './util/schemas';
import { IRouter, StartServicesAccessor } from '../../../../../core/server';
+import { RuntimeField } from '../../../common';
import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin';
const indexPatternUpdateSchema = schema.object({
@@ -139,7 +140,7 @@ export const registerUpdateIndexPatternRoute = (
if (runtimeFieldMap !== undefined) {
changeCount++;
- indexPattern.replaceAllRuntimeFields(runtimeFieldMap);
+ indexPattern.replaceAllRuntimeFields(runtimeFieldMap as Record);
}
if (changeCount < 1) {
diff --git a/src/plugins/data/server/index_patterns/routes/util/schemas.ts b/src/plugins/data/server/index_patterns/routes/util/schemas.ts
index 79ee1ffa1ab97..f378832254047 100644
--- a/src/plugins/data/server/index_patterns/routes/util/schemas.ts
+++ b/src/plugins/data/server/index_patterns/routes/util/schemas.ts
@@ -72,5 +72,28 @@ export const runtimeFieldSpec = {
source: schema.string(),
})
),
+ fields: schema.maybe(
+ schema.recordOf(
+ schema.string(),
+ schema.object({
+ type: runtimeFieldSpecTypeSchema,
+ format: schema.maybe(serializedFieldFormatSchema),
+ customLabel: schema.maybe(schema.string()),
+ popularity: schema.maybe(
+ schema.number({
+ min: 0,
+ })
+ ),
+ })
+ )
+ ),
+ format: schema.maybe(serializedFieldFormatSchema),
+ customLabel: schema.maybe(schema.string()),
+ popularity: schema.maybe(
+ schema.number({
+ min: 0,
+ })
+ ),
};
+
export const runtimeFieldSpecSchema = schema.object(runtimeFieldSpec);
diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx
index 4a4c42f69fc8e..73a0f6d997de0 100644
--- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx
+++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx
@@ -98,7 +98,7 @@ describe('', () => {
test('should accept a defaultValue and onChange prop to forward the form state', async () => {
const field = {
name: 'foo',
- type: 'date',
+ type: 'date' as const,
script: { source: 'emit("hello")' },
};
@@ -112,7 +112,7 @@ describe('', () => {
expect(lastState.submit).toBeDefined();
const { data: formData } = await submitFormAndGetData(lastState);
- expect(formData).toEqual(field);
+ expect(formData).toEqual({ ...field, format: null });
// Make sure that both isValid and isSubmitted state are now "true"
lastState = getLastStateUpdate();
@@ -128,7 +128,10 @@ describe('', () => {
onChange,
},
{
- namesNotAllowed: existingFields,
+ namesNotAllowed: {
+ fields: existingFields,
+ runtimeComposites: [],
+ },
existingConcreteFields: [],
fieldTypeToProcess: 'runtime',
}
@@ -165,7 +168,10 @@ describe('', () => {
onChange,
},
{
- namesNotAllowed: existingRuntimeFieldNames,
+ namesNotAllowed: {
+ fields: existingRuntimeFieldNames,
+ runtimeComposites: [],
+ },
existingConcreteFields: [],
fieldTypeToProcess: 'runtime',
}
diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts
index 9b00ff762fe8f..75ac0b75c444f 100644
--- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts
+++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts
@@ -33,7 +33,7 @@ describe('', () => {
test('should allow a field to be provided', async () => {
const field = {
name: 'foo',
- type: 'ip',
+ type: 'ip' as const,
script: {
source: 'emit("hello world")',
},
@@ -50,7 +50,7 @@ describe('', () => {
test('should accept an "onSave" prop', async () => {
const field = {
name: 'foo',
- type: 'date',
+ type: 'date' as const,
script: { source: 'test=123' },
};
const onSave: jest.Mock = jest.fn();
@@ -69,7 +69,7 @@ describe('', () => {
expect(onSave).toHaveBeenCalled();
const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0];
- expect(fieldReturned).toEqual(field);
+ expect(fieldReturned).toEqual({ ...field, format: null });
});
test('should accept an onCancel prop', async () => {
@@ -135,6 +135,7 @@ describe('', () => {
name: 'someName',
type: 'keyword', // default to keyword
script: { source: 'echo("hello")' },
+ format: null,
});
// Change the type and make sure it is forwarded
@@ -150,6 +151,7 @@ describe('', () => {
name: 'someName',
type: 'other_type',
script: { source: 'echo("hello")' },
+ format: null,
});
});
});
diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts
index 65089bc24317b..33e74aec1bb83 100644
--- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts
+++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts
@@ -280,7 +280,7 @@ describe('Field editor Preview panel', () => {
test('should **not** display an empty prompt editing a document with a script', async () => {
const field = {
name: 'foo',
- type: 'ip',
+ type: 'ip' as const,
script: {
source: 'emit("hello world")',
},
@@ -303,7 +303,7 @@ describe('Field editor Preview panel', () => {
test('should **not** display an empty prompt editing a document with format defined', async () => {
const field = {
name: 'foo',
- type: 'ip',
+ type: 'ip' as const,
format: {
id: 'upper',
params: {},
diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx
index d87b49d35c68e..cf9a8d5406ac2 100644
--- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx
@@ -94,7 +94,7 @@ export const WithFieldEditorDependencies = {
fieldType = [{ label: label ?? field.type, value: field.type as RuntimeType }];
}
+ const format = field.format === null ? undefined : field.format;
+
return {
...field,
type: fieldType,
+ format,
__meta__: {
isCustomLabelVisible: field.customLabel !== undefined,
isValueVisible: field.script !== undefined,
@@ -143,9 +146,12 @@ const formDeserializer = (field: Field): FieldFormInternal => {
};
const formSerializer = (field: FieldFormInternal): Field => {
- const { __meta__, type, ...rest } = field;
+ const { __meta__, type, format, ...rest } = field;
return {
type: type[0].value!,
+ // By passing "null" we are explicitly telling DataView to remove the
+ // format if there is one defined for the field.
+ format: format === undefined ? null : format,
...rest,
};
};
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx
index 2ff4a48477def..3feef82533455 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx
@@ -8,11 +8,16 @@
import React, { useState, useEffect, useRef } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
-import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports';
+import {
+ UseField,
+ useFormData,
+ ES_FIELD_TYPES,
+ useFormContext,
+ SerializedFieldFormat,
+} from '../../../shared_imports';
import { useFieldEditorContext } from '../../field_editor_context';
import { FormatSelectEditor } from '../../field_format_editor';
import type { FieldFormInternal } from '../field_editor';
-import type { FieldFormatConfig } from '../../../types';
export const FormatField = () => {
const { indexPattern, uiSettings, fieldFormats, fieldFormatEditors } = useFieldEditorContext();
@@ -44,7 +49,7 @@ export const FormatField = () => {
}, [type, getFields]);
return (
- path="format">
+ path="format">
{({ setValue, errors, value }) => {
return (
<>
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts
index ba44682ba65e0..44455d958a7cd 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts
@@ -9,14 +9,15 @@
import { i18n } from '@kbn/i18n';
import { ValidationFunc, FieldConfig } from '../../shared_imports';
-import { Field } from '../../types';
+import type { Field } from '../../types';
+import type { Context } from '../field_editor_context';
import { schema } from './form_schema';
import type { Props } from './field_editor';
const createNameNotAllowedValidator = (
- namesNotAllowed: string[]
+ namesNotAllowed: Context['namesNotAllowed']
): ValidationFunc<{}, string, string> => ({ value }) => {
- if (namesNotAllowed.includes(value)) {
+ if (namesNotAllowed.fields.includes(value)) {
return {
message: i18n.translate(
'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage',
@@ -25,6 +26,15 @@ const createNameNotAllowedValidator = (
}
),
};
+ } else if (namesNotAllowed.runtimeComposites.includes(value)) {
+ return {
+ message: i18n.translate(
+ 'indexPatternFieldEditor.editor.runtimeFieldsEditor.existCompositeNamesValidationErrorMessage',
+ {
+ defaultMessage: 'A runtime composite with this name already exists.',
+ }
+ ),
+ };
}
};
@@ -36,7 +46,7 @@ const createNameNotAllowedValidator = (
* @param field Initial value of the form
*/
export const getNameFieldConfig = (
- namesNotAllowed?: string[],
+ namesNotAllowed?: Context['namesNotAllowed'],
field?: Props['field']
): FieldConfig => {
const nameFieldConfig = schema.name as FieldConfig;
@@ -45,15 +55,18 @@ export const getNameFieldConfig = (
return nameFieldConfig;
}
+ const filterOutCurrentFieldName = (name: string) => name !== field?.name;
+
// Add validation to not allow duplicates
return {
...nameFieldConfig!,
validations: [
...(nameFieldConfig.validations ?? []),
{
- validator: createNameNotAllowedValidator(
- namesNotAllowed.filter((name) => name !== field?.name)
- ),
+ validator: createNameNotAllowedValidator({
+ fields: namesNotAllowed.fields.filter(filterOutCurrentFieldName),
+ runtimeComposites: namesNotAllowed.runtimeComposites.filter(filterOutCurrentFieldName),
+ }),
},
],
};
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx
index 74bf2657ba3de..88f9352465133 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx
@@ -31,7 +31,10 @@ export interface Context {
* e.g we probably don't want a user to give a name of an existing
* runtime field (for that the user should edit the existing runtime field).
*/
- namesNotAllowed: string[];
+ namesNotAllowed: {
+ fields: string[];
+ runtimeComposites: string[];
+ };
/**
* An array of existing concrete fields. If the user gives a name to the runtime
* field that matches one of the concrete fields, a callout will be displayed
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
index 19015aa9d0d10..65521836db7d5 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
@@ -7,6 +7,7 @@
*/
import React, { useState, useCallback, useMemo, useEffect } from 'react';
+import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -20,9 +21,10 @@ import {
EuiText,
} from '@elastic/eui';
-import type { Field, EsRuntimeField } from '../types';
+import type { Field } from '../types';
import { RuntimeFieldPainlessError } from '../lib';
import { euiFlyoutClassname } from '../constants';
+import type { RuntimeFieldSubField } from '../shared_imports';
import { FlyoutPanels } from './flyout_panels';
import { useFieldEditorContext } from './field_editor_context';
import { FieldEditor, FieldEditorFormState } from './field_editor/field_editor';
@@ -56,7 +58,9 @@ export interface Props {
*/
onCancel: () => void;
/** Handler to validate the script */
- runtimeFieldValidator: (field: EsRuntimeField) => Promise;
+ runtimeFieldValidator: (
+ field: estypes.MappingRuntimeField
+ ) => Promise;
/** Optional field to process */
field?: Field;
isSavingField: boolean;
@@ -78,6 +82,7 @@ const FieldEditorFlyoutContentComponent = ({
const { indexPattern } = useFieldEditorContext();
const {
panel: { isVisible: isPanelVisible },
+ fieldsInScript,
} = useFieldPreviewContext();
const [formState, setFormState] = useState({
@@ -119,18 +124,55 @@ const FieldEditorFlyoutContentComponent = ({
return !isFormModified;
}, [isFormModified]);
+ const addSubfieldsToField = useCallback(
+ (_field: Field): Field => {
+ const { name, type, script, format } = _field;
+
+ // This is a **temporary hack** to create the subfields.
+ // It will be replaced by a UI where the user will set the type and format
+ // of each subField.
+ if (type === 'composite' && fieldsInScript.length > 0) {
+ const fields = fieldsInScript.reduce>(
+ (acc, subFieldName) => {
+ const subField: RuntimeFieldSubField = {
+ type: 'keyword' as const,
+ format,
+ };
+
+ acc[subFieldName] = subField;
+ return acc;
+ },
+ {}
+ );
+
+ const updatedField: Field = {
+ name,
+ type,
+ script,
+ fields,
+ };
+
+ return updatedField;
+ }
+
+ return _field;
+ },
+ [fieldsInScript]
+ );
+
const onClickSave = useCallback(async () => {
- const { isValid, data } = await submit();
- const nameChange = field?.name !== data.name;
- const typeChange = field?.type !== data.type;
+ const { isValid, data: updatedField } = await submit();
+ const nameChange = field?.name !== updatedField.name;
+ const typeChange = field?.type !== updatedField.type;
if (isValid) {
- if (data.script) {
+ if (updatedField.script) {
setIsValidating(true);
const error = await runtimeFieldValidator({
- type: data.type,
- script: data.script,
+ // @ts-expect-error @elastic/elasticsearch does not support "composite" type yet
+ type: updatedField.type,
+ script: updatedField.script,
});
setIsValidating(false);
@@ -147,10 +189,10 @@ const FieldEditorFlyoutContentComponent = ({
confirmChangeNameOrType: true,
});
} else {
- onSave(data);
+ onSave(addSubfieldsToField(updatedField));
}
}
- }, [onSave, submit, runtimeFieldValidator, field, isEditingExistingField]);
+ }, [onSave, submit, runtimeFieldValidator, field, isEditingExistingField, addSubfieldsToField]);
const onClickCancel = useCallback(() => {
const canClose = canCloseValidator();
@@ -166,8 +208,8 @@ const FieldEditorFlyoutContentComponent = ({
{
- const { data } = await submit();
- onSave(data);
+ const { data: updatedField } = await submit();
+ onSave(addSubfieldsToField(updatedField));
}}
onCancel={() => {
setModalVisibility(defaultModalVisibility);
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx
index cf2b29bbc97e8..a3587ea2b79a8 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx
@@ -15,12 +15,11 @@ import {
IndexPatternField,
IndexPattern,
DataPublicPluginStart,
- RuntimeType,
UsageCollectionStart,
} from '../shared_imports';
import type { Field, PluginStart, InternalFieldType } from '../types';
import { pluginName } from '../constants';
-import { deserializeField, getRuntimeFieldValidator, getLinks, ApiService } from '../lib';
+import { getRuntimeFieldValidator, getLinks, ApiService } from '../lib';
import {
FieldEditorFlyoutContent,
Props as FieldEditorFlyoutContentProps,
@@ -30,7 +29,7 @@ import { FieldPreviewProvider } from './preview';
export interface Props {
/** Handler for the "save" footer button */
- onSave: (field: IndexPatternField) => void;
+ onSave: (field: IndexPatternField[]) => void;
/** Handler for the "cancel" footer button */
onCancel: () => void;
onMounted?: FieldEditorFlyoutContentProps['onMounted'];
@@ -41,7 +40,7 @@ export interface Props {
/** The Kibana field type of the field to create or edit (default: "runtime") */
fieldTypeToProcess: InternalFieldType;
/** Optional field to edit */
- field?: IndexPatternField;
+ field?: Field;
/** Services */
indexPatternService: DataPublicPluginStart['indexPatterns'];
notifications: NotificationsStart;
@@ -78,12 +77,20 @@ export const FieldEditorFlyoutContentContainer = ({
fieldFormats,
uiSettings,
}: Props) => {
- const fieldToEdit = deserializeField(indexPattern, field);
const [isSaving, setIsSaving] = useState(false);
const { fields } = indexPattern;
- const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]);
+ const namesNotAllowed = useMemo(() => {
+ const fieldNames = indexPattern.fields.map((fld) => fld.name);
+ const runtimeCompositeNames = Object.entries(indexPattern.getAllRuntimeFields())
+ .filter(([, _runtimeField]) => _runtimeField.type === 'composite')
+ .map(([_runtimeFieldName]) => _runtimeFieldName);
+ return {
+ fields: fieldNames,
+ runtimeComposites: runtimeCompositeNames,
+ };
+ }, [indexPattern]);
const existingConcreteFields = useMemo(() => {
const existing: Array<{ name: string; type: string }> = [];
@@ -117,59 +124,78 @@ export const FieldEditorFlyoutContentContainer = ({
[apiService, search, notifications]
);
- const saveField = useCallback(
- async (updatedField: Field) => {
- setIsSaving(true);
+ const updateRuntimeField = useCallback(
+ (updatedField: Field): IndexPatternField[] => {
+ const nameHasChanged = Boolean(field) && field!.name !== updatedField.name;
+ const typeHasChanged = Boolean(field) && field!.type !== updatedField.type;
+ const hasChangeToOrFromComposite =
+ typeHasChanged && (field!.type === 'composite' || updatedField.type === 'composite');
+
+ if (nameHasChanged || hasChangeToOrFromComposite) {
+ // Rename an existing runtime field or the type has changed from being a "composite"
+ // to any other type or from any other type to "composite"
+ indexPattern.removeRuntimeField(field!.name);
+ }
- const { script } = updatedField;
-
- if (fieldTypeToProcess === 'runtime') {
- try {
- usageCollection.reportUiCounter(pluginName, METRIC_TYPE.COUNT, 'save_runtime');
- // eslint-disable-next-line no-empty
- } catch {}
- // rename an existing runtime field
- if (field?.name && field.name !== updatedField.name) {
- indexPattern.removeRuntimeField(field.name);
- }
-
- indexPattern.addRuntimeField(updatedField.name, {
- type: updatedField.type as RuntimeType,
- script,
- });
+ return indexPattern.addRuntimeField(updatedField.name, updatedField);
+ },
+ [field, indexPattern]
+ );
+
+ const updateConcreteField = useCallback(
+ (updatedField: Field): IndexPatternField[] => {
+ const editedField = indexPattern.getFieldByName(updatedField.name);
+
+ if (!editedField) {
+ throw new Error(
+ `Unable to find field named '${updatedField.name}' on index pattern '${indexPattern.title}'`
+ );
+ }
+
+ // Update custom label, popularity and format
+ indexPattern.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
+
+ editedField.count = updatedField.popularity || 0;
+ if (updatedField.format) {
+ indexPattern.setFieldFormat(updatedField.name, updatedField.format!);
} else {
- try {
- usageCollection.reportUiCounter(pluginName, METRIC_TYPE.COUNT, 'save_concrete');
- // eslint-disable-next-line no-empty
- } catch {}
+ indexPattern.deleteFieldFormat(updatedField.name);
}
- const editedField = indexPattern.getFieldByName(updatedField.name);
+ return [editedField];
+ },
+ [indexPattern]
+ );
+ const saveField = useCallback(
+ async (updatedField: Field) => {
try {
- if (!editedField) {
- throw new Error(
- `Unable to find field named '${updatedField.name}' on index pattern '${indexPattern.title}'`
- );
- }
-
- indexPattern.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
- editedField.count = updatedField.popularity || 0;
- if (updatedField.format) {
- indexPattern.setFieldFormat(updatedField.name, updatedField.format);
- } else {
- indexPattern.deleteFieldFormat(updatedField.name);
- }
-
- await indexPatternService.updateSavedObject(indexPattern).then(() => {
- const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', {
- defaultMessage: "Saved '{fieldName}'",
- values: { fieldName: updatedField.name },
- });
- notifications.toasts.addSuccess(message);
- setIsSaving(false);
- onSave(editedField);
+ usageCollection.reportUiCounter(
+ pluginName,
+ METRIC_TYPE.COUNT,
+ fieldTypeToProcess === 'runtime' ? 'save_runtime' : 'save_concrete'
+ );
+ // eslint-disable-next-line no-empty
+ } catch {}
+
+ setIsSaving(true);
+
+ try {
+ const editedFields: IndexPatternField[] =
+ fieldTypeToProcess === 'runtime'
+ ? updateRuntimeField(updatedField)
+ : updateConcreteField(updatedField as Field);
+
+ await indexPatternService.updateSavedObject(indexPattern);
+
+ const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', {
+ defaultMessage: "Saved '{fieldName}'",
+ values: { fieldName: updatedField.name },
});
+ notifications.toasts.addSuccess(message);
+
+ setIsSaving(false);
+ onSave(editedFields);
} catch (e) {
const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', {
defaultMessage: 'Failed to save field changes',
@@ -184,7 +210,8 @@ export const FieldEditorFlyoutContentContainer = ({
indexPatternService,
notifications,
fieldTypeToProcess,
- field?.name,
+ updateConcreteField,
+ updateRuntimeField,
usageCollection,
]
);
@@ -206,7 +233,7 @@ export const FieldEditorFlyoutContentContainer = ({
onSave={saveField}
onCancel={onCancel}
onMounted={onMounted}
- field={fieldToEdit}
+ field={field}
runtimeFieldValidator={validateRuntimeField}
isSavingField={isSaving}
/>
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx
index 1c0c7ecba3b2b..bbd9fe88f818f 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx
@@ -8,7 +8,6 @@
import React, { PureComponent } from 'react';
import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui';
-
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
@@ -19,10 +18,11 @@ import {
} from 'src/plugins/data/public';
import type { FieldFormatInstanceType } from 'src/plugins/field_formats/common';
import { CoreStart } from 'src/core/public';
+
import { castEsToKbnFieldTypeName } from '../../../../data/public';
-import { FormatEditor } from './format_editor';
import { FormatEditorServiceStart } from '../../service';
-import { FieldFormatConfig } from '../../types';
+import { SerializedFieldFormat } from '../../shared_imports';
+import { FormatEditor } from './format_editor';
export interface FormatSelectEditorProps {
esTypes: ES_FIELD_TYPES[];
@@ -30,9 +30,9 @@ export interface FormatSelectEditorProps {
fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors'];
fieldFormats: DataPublicPluginStart['fieldFormats'];
uiSettings: CoreStart['uiSettings'];
- onChange: (change?: FieldFormatConfig) => void;
+ onChange: (change?: SerializedFieldFormat) => void;
onError: (error?: string) => void;
- value?: FieldFormatConfig;
+ value?: SerializedFieldFormat;
}
interface FieldTypeFormat {
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx
index c6f5fc9899ac7..3ea9dcf3dbd5a 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx
@@ -22,7 +22,7 @@ export interface FormatEditorProps {
onError: (error?: string) => void;
}
-interface FormatEditorState {
+export interface FormatEditorState {
EditorComponent: LazyExoticComponent | null;
fieldFormatId?: string;
}
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts
index 34619f53e9eed..d0f41c8c35fbe 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts
+++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts
@@ -7,4 +7,6 @@
*/
export { FormatSelectEditor, FormatSelectEditorProps } from './field_format_editor';
+export type { FormatEditorState } from './format_editor';
+export type { Sample } from './types';
export * from './editors';
diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.tsx
index 09bacf2a46096..7fc8dae1f025c 100644
--- a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.tsx
@@ -27,6 +27,7 @@ export const FieldPreview = () => {
params: {
value: { name, script, format },
},
+ isLoadingPreview,
fields,
error,
reset,
@@ -34,15 +35,15 @@ export const FieldPreview = () => {
// To show the preview we at least need a name to be defined, the script or the format
// and an first response from the _execute API
- const isEmptyPromptVisible =
- name === null && script === null && format === null
- ? true
- : // If we have some result from the _execute API call don't show the empty prompt
- error !== null || fields.length > 0
- ? false
- : name === null && format === null
- ? true
- : false;
+ let isEmptyPromptVisible = false;
+ const noParamDefined = name === null && script === null && format === null;
+ const haveResultFromPreview = error !== null || fields.length > 0;
+
+ if (noParamDefined) {
+ isEmptyPromptVisible = true;
+ } else if (!haveResultFromPreview && !isLoadingPreview && name === null && format === null) {
+ isEmptyPromptVisible = true;
+ }
const onFieldListResize = useCallback(({ height }: { height: number }) => {
setFieldListHeight(height);
@@ -53,13 +54,13 @@ export const FieldPreview = () => {
return null;
}
- const [field] = fields;
-
return (
- -
-
-
+ {fields.map((field, i) => (
+ -
+
+
+ ))}
);
};
diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx
index e1fc4b05883f4..78d92e9085f44 100644
--- a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx
@@ -20,9 +20,9 @@ import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
-import type { FieldPreviewContext, FieldFormatConfig } from '../../types';
+import type { FieldPreviewContext } from '../../types';
import { parseEsError } from '../../lib/runtime_field_validation';
-import { RuntimeType, RuntimeField } from '../../shared_imports';
+import { RuntimeType, SerializedFieldFormat } from '../../shared_imports';
import { useFieldEditorContext } from '../field_editor_context';
type From = 'cluster' | 'custom';
@@ -46,8 +46,8 @@ interface Params {
name: string | null;
index: string | null;
type: RuntimeType | null;
- script: Required['script'] | null;
- format: FieldFormatConfig | null;
+ script: { source: string } | null;
+ format: SerializedFieldFormat | null;
document: EsDocument | null;
}
@@ -94,6 +94,8 @@ interface Context {
value: { [key: string]: boolean };
set: React.Dispatch>;
};
+ /** List of fields detected in the Painless script */
+ fieldsInScript: string[];
}
const fieldPreviewContext = createContext(undefined);
@@ -157,6 +159,8 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
const [from, setFrom] = useState('cluster');
/** Map of fields pinned to the top of the list */
const [pinnedFields, setPinnedFields] = useState<{ [key: string]: boolean }>({});
+ /** Array of fields detected in the script (returned by the _execute API) */
+ const [fieldsInScript, setFieldsInScript] = useState([]);
const { documents, currentIdx } = clusterData;
const currentDocument: EsDocument | undefined = useMemo(() => documents[currentIdx], [
@@ -317,6 +321,53 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
[indexPattern, search]
);
+ const updateSingleFieldPreview = useCallback(
+ (fieldName: string, values: unknown[]) => {
+ const [value] = values;
+ const formattedValue = valueFormatter(value);
+
+ setPreviewResponse({
+ fields: [{ key: fieldName, value, formattedValue }],
+ error: null,
+ });
+ },
+ [valueFormatter]
+ );
+
+ const updateCompositeFieldPreview = useCallback(
+ (compositeName: string | null, compositeValues: Record) => {
+ if (typeof compositeValues !== 'object') {
+ return;
+ }
+
+ const updatedFieldsInScript: string[] = [];
+
+ const fields = Object.entries(compositeValues).map(([key, values]) => {
+ // The Painless _execute API returns the composite field values under a map.
+ // Each of the key is prefixed with "composite_field." (e.g. "composite_field.field1: ['value']")
+ const { 1: fieldName } = key.split('composite_field.');
+ updatedFieldsInScript.push(fieldName);
+
+ const [value] = values;
+ const formattedValue = valueFormatter(value);
+
+ return {
+ key: `${compositeName ?? ''}.${fieldName}`,
+ value,
+ formattedValue,
+ };
+ });
+
+ setPreviewResponse({
+ fields,
+ error: null,
+ });
+
+ setFieldsInScript(updatedFieldsInScript);
+ },
+ [valueFormatter]
+ );
+
const updatePreview = useCallback(async () => {
setLastExecutePainlessReqParams({
type: params.type,
@@ -371,13 +422,11 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error, true) ?? fallBackError },
});
} else {
- const [value] = values;
- const formattedValue = valueFormatter(value);
-
- setPreviewResponse({
- fields: [{ key: params.name!, value, formattedValue }],
- error: null,
- });
+ if (!Array.isArray(values)) {
+ updateCompositeFieldPreview(params.name, values);
+ return;
+ }
+ updateSingleFieldPreview(params.name!, values);
}
}, [
needToUpdatePreview,
@@ -386,7 +435,8 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
currentDocId,
getFieldPreview,
notifications.toasts,
- valueFormatter,
+ updateSingleFieldPreview,
+ updateCompositeFieldPreview,
]);
const goToNextDoc = useCallback(() => {
@@ -463,6 +513,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
value: pinnedFields,
set: setPinnedFields,
},
+ fieldsInScript,
}),
[
previewResponse,
@@ -482,6 +533,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
from,
reset,
pinnedFields,
+ fieldsInScript,
]
);
@@ -543,24 +595,64 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
}, [currentDocument, updateParams]);
/**
- * Whenever the name or the format changes we immediately update the preview
+ * Whenever the name changes we immediately update the preview
*/
useEffect(() => {
setPreviewResponse((prev) => {
- const {
- fields: { 0: field },
- } = prev;
+ const { fields } = prev;
+
+ let updatedFields: Context['fields'] = fields.map((field) => {
+ let key: string = name ?? '';
+
+ if (type === 'composite') {
+ const { 1: fieldName } = field.key.split('.');
+ key = `${name ?? ''}.${fieldName}`;
+ }
+
+ return {
+ ...field,
+ key,
+ };
+ });
- const nextValue =
- script === null && Boolean(document)
- ? get(document, name ?? '') // When there is no script we read the value from _source
- : field?.value;
+ // If the user has entered a name but not yet any script we will display
+ // the field in the preview with just the name (and a "-" for the value)
+ if (updatedFields.length === 0 && name !== null) {
+ updatedFields = [
+ { key: name, value: undefined, formattedValue: defaultValueFormatter(undefined) },
+ ];
+ }
- const formattedValue = valueFormatter(nextValue);
+ return {
+ ...prev,
+ fields: updatedFields,
+ };
+ });
+ }, [name, type]);
+
+ /**
+ * Whenever the format changes we immediately update the preview
+ */
+ useEffect(() => {
+ setPreviewResponse((prev) => {
+ const { fields } = prev;
return {
...prev,
- fields: [{ ...field, key: name ?? '', value: nextValue, formattedValue }],
+ fields: fields.map((field) => {
+ const nextValue =
+ script === null && Boolean(document)
+ ? get(document, name ?? '') // When there is no script we try to read the value from _source
+ : field?.value;
+
+ const formattedValue = valueFormatter(nextValue);
+
+ return {
+ ...field,
+ value: nextValue,
+ formattedValue,
+ };
+ }),
};
});
}, [name, script, document, valueFormatter]);
diff --git a/src/plugins/index_pattern_field_editor/public/index.ts b/src/plugins/index_pattern_field_editor/public/index.ts
index 6546dabcb2c44..a31ae03d7df49 100644
--- a/src/plugins/index_pattern_field_editor/public/index.ts
+++ b/src/plugins/index_pattern_field_editor/public/index.ts
@@ -25,7 +25,13 @@ export type {
PluginStart as IndexPatternFieldEditorStart,
} from './types';
export { DefaultFormatEditor } from './components/field_format_editor/editors/default/default';
-export { FieldFormatEditorFactory, FieldFormatEditor, FormatEditorProps } from './components';
+export { FieldFormatEditorFactory, FieldFormatEditor } from './components';
+export type {
+ DeleteFieldProviderProps,
+ FormatEditorProps,
+ FormatEditorState,
+ Sample,
+} from './components';
export function plugin() {
return new IndexPatternFieldEditorPlugin();
diff --git a/src/plugins/index_pattern_field_editor/public/lib/index.ts b/src/plugins/index_pattern_field_editor/public/lib/index.ts
index 336de9574c460..0b94434f67f0a 100644
--- a/src/plugins/index_pattern_field_editor/public/lib/index.ts
+++ b/src/plugins/index_pattern_field_editor/public/lib/index.ts
@@ -6,8 +6,6 @@
* Side Public License, v 1.
*/
-export { deserializeField } from './serialization';
-
export { getLinks } from './documentation';
export {
diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts
index b25d47b3d0d15..485e4960605cd 100644
--- a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts
+++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts
@@ -13,7 +13,7 @@ const dataStart = dataPluginMock.createStartContract();
const { search } = dataStart;
const runtimeField = {
- type: 'keyword',
+ type: 'keyword' as const,
script: {
source: 'emit("hello")',
},
diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts
index 789c4f7fa71fc..244df1ee10881 100644
--- a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts
+++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts
@@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
+import { estypes } from '@elastic/elasticsearch';
import { DataPublicPluginStart } from '../shared_imports';
-import type { EsRuntimeField } from '../types';
export interface RuntimeFieldPainlessError {
message: string;
@@ -95,7 +95,7 @@ export const parseEsError = (
export const getRuntimeFieldValidator = (
index: string,
searchService: DataPublicPluginStart['search']
-) => async (runtimeField: EsRuntimeField) => {
+) => async (runtimeField: estypes.MappingRuntimeField) => {
return await searchService
.search({
params: {
diff --git a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts
deleted file mode 100644
index 8a0a47e07c9c9..0000000000000
--- a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts
+++ /dev/null
@@ -1,28 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { IndexPatternField, IndexPattern } from '../shared_imports';
-import type { Field } from '../types';
-
-export const deserializeField = (
- indexPattern: IndexPattern,
- field?: IndexPatternField
-): Field | undefined => {
- if (field === undefined) {
- return undefined;
- }
-
- return {
- name: field.name,
- type: field?.esTypes ? field.esTypes[0] : 'keyword',
- script: field.runtimeField ? field.runtimeField.script : undefined,
- customLabel: field.customLabel,
- popularity: field.count,
- format: indexPattern.getFormatterForFieldNoDefault(field.name)?.toJSON(),
- };
-};
diff --git a/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx b/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx
index 72dbb76863353..06018c010b1c9 100644
--- a/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx
+++ b/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx
@@ -40,6 +40,18 @@ export const getFieldDeleteModalOpener = ({
indexPatternService,
usageCollection,
}: Dependencies) => (options: OpenFieldDeleteModalOptions): CloseEditor => {
+ if (typeof options.fieldName === 'string') {
+ const fieldToDelete = options.ctx.indexPattern.getFieldByName(options.fieldName);
+ const doesBelongToCompositeField = fieldToDelete?.runtimeField?.type === 'composite';
+
+ if (doesBelongToCompositeField) {
+ console.log( // eslint-disable-line
+ 'TODO: display a modal to indicate that this field needs to be deleted through its parent.'
+ );
+ return () => undefined;
+ }
+ }
+
const { overlays, notifications } = core;
let overlayRef: OverlayRef | null = null;
diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx
index 946e666bf8205..01d89585ce24c 100644
--- a/src/plugins/index_pattern_field_editor/public/open_editor.tsx
+++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx
@@ -17,9 +17,10 @@ import {
DataPublicPluginStart,
IndexPattern,
UsageCollectionStart,
+ RuntimeType,
} from './shared_imports';
-import type { PluginStart, InternalFieldType, CloseEditor } from './types';
+import type { PluginStart, InternalFieldType, CloseEditor, Field } from './types';
import type { ApiService } from './lib/api';
import { euiFlyoutClassname } from './constants';
import { FieldEditorLoader } from './components/field_editor_loader';
@@ -28,7 +29,7 @@ export interface OpenFieldEditorOptions {
ctx: {
indexPattern: IndexPattern;
};
- onSave?: (field: IndexPatternField) => void;
+ onSave?: (field: IndexPatternField[]) => void;
fieldName?: string;
}
@@ -80,7 +81,7 @@ export const getFieldEditorOpener = ({
}
};
- const onSaveField = (updatedField: IndexPatternField) => {
+ const onSaveField = (updatedField: IndexPatternField[]) => {
closeEditor();
if (onSave) {
@@ -88,9 +89,9 @@ export const getFieldEditorOpener = ({
}
};
- const field = fieldName ? indexPattern.getFieldByName(fieldName) : undefined;
+ const dataViewField = fieldName ? indexPattern.getFieldByName(fieldName) : undefined;
- if (fieldName && !field) {
+ if (fieldName && !dataViewField) {
const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', {
defaultMessage: "Field named '{fieldName}' not found on index pattern",
values: { fieldName },
@@ -100,10 +101,39 @@ export const getFieldEditorOpener = ({
}
const isNewRuntimeField = !fieldName;
- const isExistingRuntimeField = field && field.runtimeField && !field.isMapped;
+ const isExistingRuntimeField =
+ dataViewField && dataViewField.runtimeField && !dataViewField.isMapped;
const fieldTypeToProcess: InternalFieldType =
isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete';
+ let field: Field | undefined;
+ if (dataViewField) {
+ if (isExistingRuntimeField && dataViewField.runtimeField!.type === 'composite') {
+ // We are editing a composite runtime **subField**.
+ // We need to access the parent composite.
+ const [compositeName] = fieldName!.split('.');
+ field = {
+ name: compositeName,
+ ...indexPattern.getRuntimeField(compositeName)!,
+ };
+ } else if (isExistingRuntimeField) {
+ // Runtime field
+ field = {
+ name: fieldName!,
+ ...indexPattern.getRuntimeField(fieldName!)!,
+ };
+ } else {
+ // Concrete field
+ field = {
+ name: fieldName!,
+ type: (dataViewField?.esTypes ? dataViewField.esTypes[0] : 'keyword') as RuntimeType,
+ customLabel: dataViewField.customLabel,
+ popularity: dataViewField.count,
+ format: indexPattern.getFormatterForFieldNoDefault(fieldName!)?.toJSON(),
+ };
+ }
+ }
+
overlayRef = overlays.openFlyout(
toMountPoint(
diff --git a/src/plugins/index_pattern_field_editor/public/shared_imports.ts b/src/plugins/index_pattern_field_editor/public/shared_imports.ts
index 2827928d1c060..c1ad6275b7599 100644
--- a/src/plugins/index_pattern_field_editor/public/shared_imports.ts
+++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts
@@ -10,11 +10,18 @@ export { IndexPattern, IndexPatternField, DataPublicPluginStart } from '../../da
export { UsageCollectionStart } from '../../usage_collection/public';
-export { RuntimeType, RuntimeField, KBN_FIELD_TYPES, ES_FIELD_TYPES } from '../../data/common';
+export {
+ RuntimeType,
+ RuntimeField,
+ RuntimeFieldSpec,
+ RuntimeFieldSubField,
+ KBN_FIELD_TYPES,
+ ES_FIELD_TYPES,
+} from '../../data/common';
export { createKibanaReactContext, toMountPoint, CodeEditor } from '../../kibana_react/public';
-export { FieldFormat } from '../../field_formats/common';
+export { FieldFormat, SerializedFieldFormat } from '../../field_formats/common';
export {
useForm,
diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts
index f7efc9d82fc48..ad1b65bedf27f 100644
--- a/src/plugins/index_pattern_field_editor/public/types.ts
+++ b/src/plugins/index_pattern_field_editor/public/types.ts
@@ -8,12 +8,7 @@
import { FunctionComponent } from 'react';
-import {
- DataPublicPluginStart,
- RuntimeField,
- RuntimeType,
- UsageCollectionStart,
-} from './shared_imports';
+import { DataPublicPluginStart, UsageCollectionStart, RuntimeField } from './shared_imports';
import { OpenFieldEditorOptions } from './open_editor';
import { OpenFieldDeleteModalOptions } from './open_delete_modal';
import { FormatEditorServiceSetup, FormatEditorServiceStart } from './service';
@@ -43,25 +38,8 @@ export interface StartPlugins {
export type InternalFieldType = 'concrete' | 'runtime';
-export interface Field {
+export interface Field extends RuntimeField {
name: string;
- type: RuntimeField['type'] | string;
- script?: RuntimeField['script'];
- customLabel?: string;
- popularity?: number;
- format?: FieldFormatConfig;
-}
-
-export interface FieldFormatConfig {
- id: string;
- params?: { [key: string]: any };
-}
-
-export interface EsRuntimeField {
- type: RuntimeType | string;
- script?: {
- source: string;
- };
}
export type CloseEditor = () => void;
diff --git a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts
index 238701904e22c..ff66d711adf96 100644
--- a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts
+++ b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts
@@ -24,6 +24,7 @@ const bodySchema = schema.object({
schema.literal('ip_field'),
schema.literal('keyword_field'),
schema.literal('long_field'),
+ schema.literal('composite_field'),
]),
document: schema.object({}, { unknowns: 'allow' }),
});
diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts
index e00cf647930a8..a40bc33ee1299 100644
--- a/src/plugins/vis_types/vega/public/data_model/search_api.ts
+++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts
@@ -32,6 +32,7 @@ export const extendSearchParamsWithRuntimeFields = async (
const indexPattern = (await indexPatterns.find(indexPatternString)).find(
(index) => index.title === indexPatternString
);
+ // @ts-expect-error The MappingRuntimeFieldType from @elastic/elasticsearch does not expose the "composite" runtime type yet
runtimeMappings = indexPattern?.getComputedFields().runtimeFields;
}
diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts
index b41a630889ff8..6f5ff46816bb5 100644
--- a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts
+++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts
@@ -56,16 +56,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(response1.status).to.be(404);
});
- it('returns error when attempting to delete a field which is not a runtime field', async () => {
- const response2 = await supertest.delete(
- `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/foo`
- );
-
- expect(response2.status).to.be(400);
- expect(response2.body.statusCode).to.be(400);
- expect(response2.body.message).to.be('Only runtime fields can be deleted.');
- });
-
it('returns error when ID is too long', async () => {
const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`;
const response = await supertest.delete(
diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts
index 3608089e4641a..d17e62630539e 100644
--- a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts
+++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts
@@ -67,15 +67,5 @@ export default function ({ getService }: FtrProviderContext) {
'[request params.id]: value has length [1759] but it must have a maximum length of [1000].'
);
});
-
- it('returns error when attempting to fetch a field which is not a runtime field', async () => {
- const response2 = await supertest.get(
- `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/foo`
- );
-
- expect(response2.status).to.be(400);
- expect(response2.body.statusCode).to.be(400);
- expect(response2.body.message).to.be('Only runtime fields can be retrieved.');
- });
});
}
diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts
index 9faca08238033..0741f1988c647 100644
--- a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts
+++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts
@@ -40,30 +40,5 @@ export default function ({ getService }: FtrProviderContext) {
expect(response.status).to.be(404);
});
-
- it('returns error on non-runtime field update attempt', async () => {
- const title = `basic_index`;
- const response1 = await supertest.post('/api/index_patterns/index_pattern').send({
- override: true,
- index_pattern: {
- title,
- },
- });
-
- const response2 = await supertest
- .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`)
- .send({
- name: 'bar',
- runtimeField: {
- type: 'long',
- script: {
- source: "emit(doc['field_name'].value)",
- },
- },
- });
-
- expect(response2.status).to.be(400);
- expect(response2.body.message).to.be('Only runtime fields can be updated');
- });
});
}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
index 1b92eaddd1343..7bce589f3baca 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
@@ -21,7 +21,6 @@ const MAX_EXAMPLES_DEFAULT: number = 10;
export class DataLoader {
private _indexPattern: IndexPattern;
- private _runtimeMappings: estypes.MappingRuntimeFields;
private _indexPatternTitle: IndexPatternTitle = '';
private _maxExamples: number = MAX_EXAMPLES_DEFAULT;
private _toastNotifications: CoreSetup['notifications']['toasts'];
@@ -31,8 +30,6 @@ export class DataLoader {
toastNotifications: CoreSetup['notifications']['toasts']
) {
this._indexPattern = indexPattern;
- this._runtimeMappings = this._indexPattern.getComputedFields()
- .runtimeFields as estypes.MappingRuntimeFields;
this._indexPatternTitle = indexPattern.title;
this._toastNotifications = toastNotifications;
}
@@ -70,7 +67,7 @@ export class DataLoader {
latest,
aggregatableFields,
nonAggregatableFields,
- runtimeMappings: this._runtimeMappings,
+ runtimeMappings: this._indexPattern.getRuntimeMappings() as estypes.MappingRuntimeFields,
});
return stats;
@@ -94,7 +91,7 @@ export class DataLoader {
interval,
fields,
maxExamples: this._maxExamples,
- runtimeMappings: this._runtimeMappings,
+ runtimeMappings: this._indexPattern.getRuntimeMappings() as estypes.MappingRuntimeFields,
});
return stats;
diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts
index ee831d9a98eb9..e6c64aef53f91 100644
--- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts
+++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts
@@ -109,6 +109,7 @@ const resolveRuntimeMappings = (indexPattern: IndexPattern): estypes.MappingRunt
const runtimeMappingsFromIndexPattern = (Object.entries(runtimeFields) as ObjectEntries<
typeof runtimeFields
>).reduce(
+ // @ts-expect-error @elasticsearch/elasticsearch does not support yet "composite" type for runtime fields
(accumulatedMappings, [runtimeFieldName, runtimeFieldSpec]) => ({
...accumulatedMappings,
[runtimeFieldName]: {
@@ -117,7 +118,7 @@ const resolveRuntimeMappings = (indexPattern: IndexPattern): estypes.MappingRunt
? {
script: {
lang: 'painless', // required in the es types
- source: runtimeFieldSpec.script.source,
+ source: (runtimeFieldSpec.script as estypes.InlineScript).source,
},
}
: {}),
@@ -126,5 +127,6 @@ const resolveRuntimeMappings = (indexPattern: IndexPattern): estypes.MappingRunt
{}
);
+ // @ts-expect-error @elasticsearch/elasticsearch does not support yet "composite" type for runtime fields
return runtimeMappingsFromIndexPattern;
};
diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts
index f35b0a7f23179..ad65b05ba66c2 100644
--- a/x-pack/plugins/lens/server/routes/existing_fields.ts
+++ b/x-pack/plugins/lens/server/routes/existing_fields.ts
@@ -115,6 +115,8 @@ async function fetchFieldExistence({
const indexPattern = await indexPatternsService.get(indexPatternId);
const fields = buildFieldList(indexPattern, metaFields);
+ const runtimeMappings = indexPattern.getRuntimeMappings();
+
const docs = await fetchIndexPatternStats({
fromDate,
toDate,
@@ -123,6 +125,8 @@ async function fetchFieldExistence({
index: indexPattern.title,
timeFieldName: timeFieldName || indexPattern.timeFieldName,
fields,
+ // @ts-expect-error The MappingRuntimeField from @elastic/elasticsearch does not expose the "composite" runtime type yet
+ runtimeMappings,
});
return {
@@ -157,6 +161,7 @@ async function fetchIndexPatternStats({
fromDate,
toDate,
fields,
+ runtimeMappings,
}: {
client: ElasticsearchClient;
index: string;
@@ -165,6 +170,7 @@ async function fetchIndexPatternStats({
fromDate?: string;
toDate?: string;
fields: Field[];
+ runtimeMappings: estypes.MappingRuntimeFields;
}) {
const filter =
timeFieldName && fromDate && toDate
@@ -188,7 +194,7 @@ async function fetchIndexPatternStats({
};
const scriptedFields = fields.filter((f) => f.isScript);
- const runtimeFields = fields.filter((f) => f.runtimeField);
+
const { body: result } = await client.search(
{
index,
@@ -199,11 +205,7 @@ async function fetchIndexPatternStats({
sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [],
fields: ['*'],
_source: false,
- runtime_mappings: runtimeFields.reduce((acc, field) => {
- if (!field.runtimeField) return acc;
- acc[field.name] = field.runtimeField;
- return acc;
- }, {} as Record),
+ runtime_mappings: runtimeMappings,
script_fields: scriptedFields.reduce((acc, field) => {
acc[field.name] = {
script: {
diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts
index 7103e395eabdc..161a22328d74e 100644
--- a/x-pack/plugins/lens/server/routes/field_stats.ts
+++ b/x-pack/plugins/lens/server/routes/field_stats.ts
@@ -83,6 +83,7 @@ export async function initFieldsRoute(setup: CoreSetup) {
.filter((f) => f.runtimeField)
.reduce((acc, f) => {
if (!f.runtimeField) return acc;
+ // @ts-expect-error The MappingRuntimeField from @elastic/elasticsearch does not expose the "composite" runtime type yet
acc[f.name] = f.runtimeField;
return acc;
}, {} as Record);
diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts
index 5354598dc2475..fdefa3a8c4ad9 100644
--- a/x-pack/plugins/transform/common/api_schemas/common.ts
+++ b/x-pack/plugins/transform/common/api_schemas/common.ts
@@ -68,6 +68,7 @@ export const runtimeMappingsSchema = schema.maybe(
schema.literal('ip'),
schema.literal('boolean'),
schema.literal('geo_point'),
+ schema.literal('composite'),
]),
script: schema.maybe(
schema.oneOf([
diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
index 55a304207a1c7..c189642ccb691 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
@@ -117,6 +117,7 @@ export const useIndexData = (
if (combinedRuntimeMappings !== undefined) {
result = Object.keys(combinedRuntimeMappings).map((fieldName) => {
const field = combinedRuntimeMappings[fieldName];
+ // @ts-expect-error @elastic/elasticsearch does not support yet "composite" type for runtime fields
const schema = getDataGridSchemaFromESFieldType(field.type);
return { id: fieldName, schema };
});
diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts
index bfe2f47078569..7b947cf5b2fd9 100644
--- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts
+++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts
@@ -41,6 +41,7 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen
query,
fields,
samplerShardSize,
+ // @ts-expect-error @elasticsearch/elasticsearch does not support yet "composite" type for runtime fields
runtimeMappings
);