diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts
index a97b8025426f2..7970f8373a42e 100644
--- a/src/plugins/data/common/index.ts
+++ b/src/plugins/data/common/index.ts
@@ -103,6 +103,7 @@ export type {
FieldFormatMap,
RuntimeType,
RuntimeField,
+ RuntimeFieldSpec,
IIndexPattern,
DataViewAttributes,
IndexPatternAttributes,
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index 02480aded9655..aae8d204a9f2e 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -81,6 +81,7 @@ export type {
IndexPatternLoadExpressionFunctionDefinition,
GetFieldsOptions,
AggregationRestrictions,
+ RuntimeType,
DataViewListItem,
} from '../common';
export {
diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx
index 55b9876ac54ad..f13811b72dcd1 100644
--- a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx
+++ b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor.test.tsx
@@ -99,7 +99,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")' },
};
@@ -113,7 +113,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();
@@ -129,7 +129,10 @@ describe('', () => {
onChange,
},
{
- namesNotAllowed: existingFields,
+ namesNotAllowed: {
+ fields: existingFields,
+ runtimeComposites: [],
+ },
existingConcreteFields: [],
fieldTypeToProcess: 'runtime',
}
@@ -166,7 +169,10 @@ describe('', () => {
onChange,
},
{
- namesNotAllowed: existingRuntimeFieldNames,
+ namesNotAllowed: {
+ fields: existingRuntimeFieldNames,
+ runtimeComposites: [],
+ },
existingConcreteFields: [],
fieldTypeToProcess: 'runtime',
}
diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts
index 1730593dbda20..c83a786d7e7a7 100644
--- a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts
+++ b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts
@@ -40,7 +40,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")',
},
@@ -57,7 +57,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();
@@ -72,7 +72,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 () => {
@@ -133,6 +133,7 @@ describe('', () => {
name: 'someName',
type: 'keyword', // default to keyword
script: { source: 'echo("hello")' },
+ format: null,
});
// Change the type and make sure it is forwarded
@@ -149,6 +150,7 @@ describe('', () => {
name: 'someName',
type: 'date',
script: { source: 'echo("hello")' },
+ format: null,
});
});
@@ -186,6 +188,7 @@ describe('', () => {
name: 'someName',
type: 'keyword',
script: { source: 'echo("hello")' },
+ format: null,
});
});
});
diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts
index c3175d7097a36..7c273286b4a5a 100644
--- a/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts
+++ b/src/plugins/data_view_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts
@@ -220,7 +220,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")',
},
@@ -241,7 +241,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/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx b/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx
index 722b1b1ec2cea..bdef57ea84e91 100644
--- a/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx
@@ -127,7 +127,7 @@ export const WithFieldEditorDependencies =
uiSettings: uiSettingsServiceMock.createStartContract(),
fieldTypeToProcess: 'runtime',
existingConcreteFields: [],
- namesNotAllowed: [],
+ namesNotAllowed: { fields: [], runtimeComposites: [] },
links: {
runtimePainless: 'https://elastic.co',
},
diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/composite_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/composite_editor.tsx
new file mode 100644
index 0000000000000..fc66d4a9c3bc4
--- /dev/null
+++ b/src/plugins/data_view_field_editor/public/components/field_editor/composite_editor.tsx
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 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 React from 'react';
+
+export const CompositeEditor = () => <>hi>;
diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts b/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts
index e262d3ecbfe45..30f3c20131ae7 100644
--- a/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts
+++ b/src/plugins/data_view_field_editor/public/components/field_editor/constants.ts
@@ -38,4 +38,8 @@ export const RUNTIME_FIELD_OPTIONS: Array>
label: 'Geo point',
value: 'geo_point',
},
+ {
+ label: 'Composite',
+ value: 'composite',
+ },
];
diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/field_detail.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/field_detail.tsx
new file mode 100644
index 0000000000000..b9db87b65e3cd
--- /dev/null
+++ b/src/plugins/data_view_field_editor/public/components/field_editor/field_detail.tsx
@@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 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 React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiCode } from '@elastic/eui';
+import { AdvancedParametersSection } from './advanced_parameters_section';
+import { FormRow } from './form_row';
+import { PopularityField, FormatField, ScriptField, CustomLabelField } from './form_fields';
+import { useFieldEditorContext } from '../field_editor_context';
+
+const geti18nTexts = (): {
+ [key: string]: { title: string; description: JSX.Element | string };
+} => ({
+ customLabel: {
+ title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', {
+ defaultMessage: 'Set custom label',
+ }),
+ description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', {
+ defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`,
+ }),
+ },
+ value: {
+ title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', {
+ defaultMessage: 'Set value',
+ }),
+ description: (
+ {'_source'},
+ }}
+ />
+ ),
+ },
+
+ format: {
+ title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', {
+ defaultMessage: 'Set format',
+ }),
+ description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', {
+ defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`,
+ }),
+ },
+
+ popularity: {
+ title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', {
+ defaultMessage: 'Set popularity',
+ }),
+ description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', {
+ defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`,
+ }),
+ },
+});
+
+export const FieldDetail = ({}) => {
+ const { links, existingConcreteFields, fieldTypeToProcess } = useFieldEditorContext();
+ const i18nTexts = geti18nTexts();
+ return (
+ <>
+ {/* Set custom label */}
+
+
+
+
+ {/* Set value */}
+ {fieldTypeToProcess === 'runtime' && (
+
+
+
+ )}
+
+ {/* Set custom format */}
+
+
+
+
+ {/* Advanced settings */}
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx
index 0185e1ba1c3f9..c06ee0ee46288 100644
--- a/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx
+++ b/src/plugins/data_view_field_editor/public/components/field_editor/field_editor.tsx
@@ -8,14 +8,12 @@
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
import { get } from 'lodash';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiComboBoxOptionOption,
- EuiCode,
EuiCallOut,
} from '@elastic/eui';
@@ -36,15 +34,9 @@ import { useFieldPreviewContext } from '../preview';
import { RUNTIME_FIELD_OPTIONS } from './constants';
import { schema } from './form_schema';
import { getNameFieldConfig } from './lib';
-import {
- TypeField,
- CustomLabelField,
- ScriptField,
- FormatField,
- PopularityField,
-} from './form_fields';
-import { FormRow } from './form_row';
-import { AdvancedParametersSection } from './advanced_parameters_section';
+import { TypeField } from './form_fields';
+import { FieldDetail } from './field_detail';
+import { CompositeEditor } from './composite_editor';
export interface FieldEditorFormState {
isValid: boolean | undefined;
@@ -72,49 +64,6 @@ export interface Props {
onFormModifiedChange?: (isModified: boolean) => void;
}
-const geti18nTexts = (): {
- [key: string]: { title: string; description: JSX.Element | string };
-} => ({
- customLabel: {
- title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', {
- defaultMessage: 'Set custom label',
- }),
- description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', {
- defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`,
- }),
- },
- value: {
- title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', {
- defaultMessage: 'Set value',
- }),
- description: (
- {'_source'},
- }}
- />
- ),
- },
- format: {
- title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', {
- defaultMessage: 'Set format',
- }),
- description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', {
- defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`,
- }),
- },
- popularity: {
- title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', {
- defaultMessage: 'Set popularity',
- }),
- description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', {
- defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`,
- }),
- },
-});
-
const changeWarning = i18n.translate('indexPatternFieldEditor.editor.form.changeWarning', {
defaultMessage:
'Changing name or type can break searches and visualizations that rely on this field.',
@@ -129,9 +78,12 @@ const formDeserializer = (field: Field): FieldFormInternal => {
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,
@@ -142,16 +94,18 @@ 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,
};
};
const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) => {
- const { links, namesNotAllowed, existingConcreteFields, fieldTypeToProcess } =
- useFieldEditorContext();
+ const { namesNotAllowed, fieldTypeToProcess } = useFieldEditorContext();
const {
params: { update: updatePreviewParams },
} = useFieldPreviewContext();
@@ -164,7 +118,6 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props)
const { submit, isValid: isFormValid, isSubmitted, getFields, isSubmitting } = form;
const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field);
- const i18nTexts = geti18nTexts();
const [formData] = useFormData({ form });
const isFormModified = useFormIsModified({
@@ -213,6 +166,8 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props)
}
}, [isFormModified, onFormModifiedChange]);
+ console.log('render', updatedType && updatedType[0].value);
+
return (
>
)}
-
-
- {/* Set custom label */}
-
-
-
- {/* Set value */}
- {fieldTypeToProcess === 'runtime' && (
-
-
-
- )}
-
- {/* Set custom format */}
-
-
-
+
- {/* Advanced settings */}
-
-
-
-
-
+ {updatedType && updatedType[0].value !== 'composite' ? : }
);
};
diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx
index c584a59eda8d1..f5880a2227439 100644
--- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/format_field.tsx
+++ b/src/plugins/data_view_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 { dataView, 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/data_view_field_editor/public/components/field_editor/lib.ts b/src/plugins/data_view_field_editor/public/components/field_editor/lib.ts
index 5b2e66c66fe39..9f2b9df86da2e 100644
--- a/src/plugins/data_view_field_editor/public/components/field_editor/lib.ts
+++ b/src/plugins/data_view_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[]): ValidationFunc<{}, string, 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/data_view_field_editor/public/components/field_editor_context.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx
index 4bee00f3c4b2a..5dd56d6512b9c 100644
--- a/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx
+++ b/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx
@@ -8,8 +8,7 @@
import React, { createContext, useContext, FunctionComponent, useMemo } from 'react';
import { NotificationsStart, CoreStart } from 'src/core/public';
-import { FieldFormatsStart } from '../shared_imports';
-import type { DataView, DataPublicPluginStart } from '../shared_imports';
+import type { DataView, DataPublicPluginStart, FieldFormatsStart } from '../shared_imports';
import { ApiService } from '../lib/api';
import type { InternalFieldType, PluginStart } from '../types';
@@ -32,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/data_view_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx
index 5d14f3a7be4cb..4453ae68602e4 100644
--- a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx
+++ b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content.tsx
@@ -20,6 +20,7 @@ import {
import type { Field } from '../types';
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';
@@ -70,6 +71,7 @@ const FieldEditorFlyoutContentComponent = ({
const { dataView } = useFieldEditorContext();
const {
panel: { isVisible: isPanelVisible },
+ fieldsInScript,
} = useFieldPreviewContext();
const [formState, setFormState] = useState({
@@ -97,8 +99,44 @@ 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 { isValid, data: updatedField } = await submit();
if (!isMounted.current) {
// User has closed the flyout meanwhile submitting the form
@@ -106,8 +144,8 @@ const FieldEditorFlyoutContentComponent = ({
}
if (isValid) {
- const nameChange = field?.name !== data.name;
- const typeChange = field?.type !== data.type;
+ const nameChange = field?.name !== updatedField.name;
+ const typeChange = field?.type !== updatedField.type;
if (isEditingExistingField && (nameChange || typeChange)) {
setModalVisibility({
@@ -115,10 +153,10 @@ const FieldEditorFlyoutContentComponent = ({
confirmChangeNameOrType: true,
});
} else {
- onSave(data);
+ onSave(addSubfieldsToField(updatedField));
}
}
- }, [onSave, submit, field, isEditingExistingField]);
+ }, [onSave, submit, field, isEditingExistingField, addSubfieldsToField]);
const onClickCancel = useCallback(() => {
const canClose = canCloseValidator();
@@ -134,8 +172,8 @@ const FieldEditorFlyoutContentComponent = ({
{
- const { data } = await submit();
- onSave(data);
+ const { data: updatedField } = await submit();
+ onSave(addSubfieldsToField(updatedField));
}}
onCancel={() => {
setModalVisibility(defaultModalVisibility);
@@ -182,6 +220,8 @@ const FieldEditorFlyoutContentComponent = ({
};
}, []);
+ console.log('renderfield editor');
+
return (
<>
void;
+ onSave: (field: DataViewField[]) => void;
/** Handler for the "cancel" footer button */
onCancel: () => void;
onMounted?: FieldEditorFlyoutContentProps['onMounted'];
@@ -43,7 +42,7 @@ export interface Props {
/** The Kibana field type of the field to create or edit (default: "runtime") */
fieldTypeToProcess: InternalFieldType;
/** Optional field to edit */
- field?: DataViewField;
+ field?: Field;
/** Services */
dataViews: DataViewsPublicPluginStart;
notifications: NotificationsStart;
@@ -80,12 +79,20 @@ export const FieldEditorFlyoutContentContainer = ({
fieldFormats,
uiSettings,
}: Props) => {
- const fieldToEdit = deserializeField(dataView, field);
const [isSaving, setIsSaving] = useState(false);
const { fields } = dataView;
- const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]);
+ const namesNotAllowed = useMemo(() => {
+ const fieldNames = dataView.fields.map((fld) => fld.name);
+ const runtimeCompositeNames = Object.entries(dataView.getAllRuntimeFields())
+ .filter(([, _runtimeField]) => _runtimeField.type === 'composite')
+ .map(([_runtimeFieldName]) => _runtimeFieldName);
+ return {
+ fields: fieldNames,
+ runtimeComposites: runtimeCompositeNames,
+ };
+ }, [dataView]);
const existingConcreteFields = useMemo(() => {
const existing: Array<{ name: string; type: string }> = [];
@@ -114,59 +121,78 @@ export const FieldEditorFlyoutContentContainer = ({
[apiService, search, notifications]
);
- const saveField = useCallback(
- async (updatedField: Field) => {
- setIsSaving(true);
+ const updateRuntimeField = useCallback(
+ (updatedField: Field): DataViewField[] => {
+ 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"
+ dataView.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) {
- dataView.removeRuntimeField(field.name);
- }
-
- dataView.addRuntimeField(updatedField.name, {
- type: updatedField.type as RuntimeType,
- script,
- });
+ return dataView.addRuntimeField(updatedField.name, updatedField);
+ },
+ [field, dataView]
+ );
+
+ const updateConcreteField = useCallback(
+ (updatedField: Field): DataViewField[] => {
+ const editedField = dataView.getFieldByName(updatedField.name);
+
+ if (!editedField) {
+ throw new Error(
+ `Unable to find field named '${updatedField.name}' on index pattern '${dataView.title}'`
+ );
+ }
+
+ // Update custom label, popularity and format
+ dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
+
+ editedField.count = updatedField.popularity || 0;
+ if (updatedField.format) {
+ dataView.setFieldFormat(updatedField.name, updatedField.format!);
} else {
- try {
- usageCollection.reportUiCounter(pluginName, METRIC_TYPE.COUNT, 'save_concrete');
- // eslint-disable-next-line no-empty
- } catch {}
+ dataView.deleteFieldFormat(updatedField.name);
}
- const editedField = dataView.getFieldByName(updatedField.name);
+ return [editedField];
+ },
+ [dataView]
+ );
+ const saveField = useCallback(
+ async (updatedField: Field) => {
try {
- if (!editedField) {
- throw new Error(
- `Unable to find field named '${updatedField.name}' on index pattern '${dataView.title}'`
- );
- }
-
- dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
- editedField.count = updatedField.popularity || 0;
- if (updatedField.format) {
- dataView.setFieldFormat(updatedField.name, updatedField.format);
- } else {
- dataView.deleteFieldFormat(updatedField.name);
- }
-
- await dataViews.updateSavedObject(dataView).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: DataViewField[] =
+ fieldTypeToProcess === 'runtime'
+ ? updateRuntimeField(updatedField)
+ : updateConcreteField(updatedField as Field);
+
+ await dataViews.updateSavedObject(dataView);
+
+ 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',
@@ -175,7 +201,16 @@ export const FieldEditorFlyoutContentContainer = ({
setIsSaving(false);
}
},
- [onSave, dataView, dataViews, notifications, fieldTypeToProcess, field?.name, usageCollection]
+ [
+ onSave,
+ dataView,
+ dataViews,
+ notifications,
+ fieldTypeToProcess,
+ updateConcreteField,
+ updateRuntimeField,
+ usageCollection,
+ ]
);
return (
@@ -195,7 +230,7 @@ export const FieldEditorFlyoutContentContainer = ({
onSave={saveField}
onCancel={onCancel}
onMounted={onMounted}
- field={fieldToEdit}
+ field={field}
isSavingField={isSaving}
/>
diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx
index e921d0beafce1..533e05ac7cd99 100644
--- a/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx
+++ b/src/plugins/data_view_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 { KBN_FIELD_TYPES, ES_FIELD_TYPES } from 'src/plugins/data/public';
@@ -17,9 +16,9 @@ import { CoreStart } from 'src/core/public';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { FieldFormatsStart } from 'src/plugins/field_formats/public';
import { DataView } from 'src/plugins/data_views/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[];
@@ -27,9 +26,9 @@ export interface FormatSelectEditorProps {
fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors'];
fieldFormats: FieldFormatsStart;
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/data_view_field_editor/public/components/field_format_editor/format_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx
index c6f5fc9899ac7..3ea9dcf3dbd5a 100644
--- a/src/plugins/data_view_field_editor/public/components/field_format_editor/format_editor.tsx
+++ b/src/plugins/data_view_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/data_view_field_editor/public/components/field_format_editor/index.ts b/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts
index 0c23c8de616cf..2ae6b3149dc5f 100644
--- a/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts
+++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/index.ts
@@ -8,4 +8,6 @@
export type { FormatSelectEditorProps } from './field_format_editor';
export { FormatSelectEditor } from './field_format_editor';
+export type { FormatEditorState } from './format_editor';
+export type { Sample } from './types';
export * from './editors';
diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx
index 05339e6473b6b..f331e62bd7016 100644
--- a/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx
+++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx
@@ -27,6 +27,7 @@ export const FieldPreview = () => {
params: {
value: { name, script, format },
},
+ isLoadingPreview,
fields,
error,
documents: { fetchDocError },
@@ -36,15 +37,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
- Boolean(error) || 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 doRenderListOfFields = fetchDocError === null;
const showWarningPreviewNotAvailable = isPreviewAvailable === false && fetchDocError === null;
@@ -58,13 +59,13 @@ export const FieldPreview = () => {
return null;
}
- const [field] = fields;
-
return (
- -
-
-
+ {fields.map((field, i) => (
+ -
+
+
+ ))}
);
};
diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx
index 9f2ca0ef53e1b..f91ceb5bf0ade 100644
--- a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx
+++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx
@@ -109,6 +109,8 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
isValid: boolean;
message: string | null;
}>({ isValidating: false, isValid: true, message: null });
+ /** Array of fields detected in the script (returned by the _execute API) */
+ const [fieldsInScript, setFieldsInScript] = useState([]);
const { documents, currentIdx } = clusterData;
const currentDocument: EsDocument | undefined = documents[currentIdx];
@@ -314,6 +316,55 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
[dataView, search]
);
+ const updateSingleFieldPreview = useCallback(
+ (fieldName: string, values: unknown[]) => {
+ console.log('SINGLE FIELD PREVIEW');
+ const [value] = values;
+ const formattedValue = valueFormatter(value);
+
+ setPreviewResponse({
+ fields: [{ key: fieldName, value, formattedValue }],
+ error: null,
+ });
+ },
+ [valueFormatter]
+ );
+
+ const updateCompositeFieldPreview = useCallback(
+ (compositeName: string | null, compositeValues: Record) => {
+ console.log('COMPOSITE FIELD VALUE');
+ 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 () => {
if (scriptEditorValidation.isValidating) {
return;
@@ -332,6 +383,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
const currentApiCall = ++previewCount.current;
+ console.log('getFieldPreview');
const response = await getFieldPreview({
index: dataView.title,
document: document!,
@@ -368,13 +420,11 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error) },
});
} else {
- const [value] = values;
- const formattedValue = valueFormatter(value);
-
- setPreviewResponse({
- fields: [{ key: name!, value, formattedValue }],
- error: null,
- });
+ if (!Array.isArray(values)) {
+ updateCompositeFieldPreview(name, values);
+ return;
+ }
+ updateSingleFieldPreview(name!, values);
}
}
@@ -387,10 +437,11 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
currentDocId,
getFieldPreview,
notifications.toasts,
- valueFormatter,
allParamsDefined,
scriptEditorValidation,
hasSomeParamsChanged,
+ updateSingleFieldPreview,
+ updateCompositeFieldPreview,
dataView.title,
]);
@@ -468,6 +519,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
validation: {
setScriptEditorValidation,
},
+ fieldsInScript,
}),
[
previewResponse,
@@ -490,6 +542,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => {
from,
reset,
pinnedFields,
+ fieldsInScript,
]
);
@@ -536,24 +589,62 @@ 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 = 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
+ if (updatedFields.length === 0 && name !== null) {
+ updatedFields = [{ key: name, value: undefined, formattedValue: undefined }];
+ }
+
+ return {
+ ...prev,
+ fields: updatedFields,
+ };
+ });
+ }, [name, type]);
- const formattedValue = valueFormatter(nextValue);
+ /**
+ * 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/data_view_field_editor/public/components/preview/types.ts b/src/plugins/data_view_field_editor/public/components/preview/types.ts
index d7c0a5867efd6..aac063c8f54ed 100644
--- a/src/plugins/data_view_field_editor/public/components/preview/types.ts
+++ b/src/plugins/data_view_field_editor/public/components/preview/types.ts
@@ -7,8 +7,8 @@
*/
import React from 'react';
-import type { RuntimeType, RuntimeField } from '../../shared_imports';
-import type { FieldFormatConfig, RuntimeFieldPainlessError } from '../../types';
+import type { RuntimeType, RuntimeField, SerializedFieldFormat } from '../../shared_imports';
+import type { RuntimeFieldPainlessError } from '../../types';
export type From = 'cluster' | 'custom';
@@ -54,7 +54,7 @@ export interface Params {
index: string | null;
type: RuntimeType | null;
script: Required['script'] | null;
- format: FieldFormatConfig | null;
+ format: SerializedFieldFormat | null;
document: { [key: string]: unknown } | null;
}
@@ -108,6 +108,8 @@ export interface Context {
React.SetStateAction<{ isValid: boolean; isValidating: boolean; message: string | null }>
>;
};
+ /** List of fields detected in the Painless script */
+ fieldsInScript: string[];
}
export type PainlessExecuteContext =
diff --git a/src/plugins/data_view_field_editor/public/index.ts b/src/plugins/data_view_field_editor/public/index.ts
index 55af27fdb29eb..284c9feccff9b 100644
--- a/src/plugins/data_view_field_editor/public/index.ts
+++ b/src/plugins/data_view_field_editor/public/index.ts
@@ -25,7 +25,14 @@ export type {
PluginStart as IndexPatternFieldEditorStart,
} from './types';
export { DefaultFormatEditor } from './components/field_format_editor/editors/default/default';
-export type { FieldFormatEditorFactory, FieldFormatEditor, FormatEditorProps } from './components';
+export type {
+ FieldFormatEditorFactory,
+ FieldFormatEditor,
+ DeleteFieldProviderProps,
+ FormatEditorProps,
+ FormatEditorState,
+ Sample,
+} from './components';
export function plugin() {
return new IndexPatternFieldEditorPlugin();
diff --git a/src/plugins/data_view_field_editor/public/lib/index.ts b/src/plugins/data_view_field_editor/public/lib/index.ts
index c7627a63da9ff..febe9a02c7749 100644
--- a/src/plugins/data_view_field_editor/public/lib/index.ts
+++ b/src/plugins/data_view_field_editor/public/lib/index.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export { deserializeField, painlessErrorToMonacoMarker } from './serialization';
+export { painlessErrorToMonacoMarker } from './serialization';
export { getLinks } from './documentation';
diff --git a/src/plugins/data_view_field_editor/public/lib/serialization.ts b/src/plugins/data_view_field_editor/public/lib/serialization.ts
index 833fe331203c2..752d7af6d7612 100644
--- a/src/plugins/data_view_field_editor/public/lib/serialization.ts
+++ b/src/plugins/data_view_field_editor/public/lib/serialization.ts
@@ -6,23 +6,7 @@
* Side Public License, v 1.
*/
import { monaco } from '@kbn/monaco';
-import { DataViewField, DataView } from '../shared_imports';
-import type { Field, RuntimeFieldPainlessError } from '../types';
-
-export const deserializeField = (dataView: DataView, field?: DataViewField): 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: dataView.getFormatterForFieldNoDefault(field.name)?.toJSON(),
- };
-};
+import type { RuntimeFieldPainlessError } from '../types';
export const painlessErrorToMonacoMarker = (
{ reason }: RuntimeFieldPainlessError,
diff --git a/src/plugins/data_view_field_editor/public/open_delete_modal.tsx b/src/plugins/data_view_field_editor/public/open_delete_modal.tsx
index f44367d16d08d..c5bc6e77a4f6e 100644
--- a/src/plugins/data_view_field_editor/public/open_delete_modal.tsx
+++ b/src/plugins/data_view_field_editor/public/open_delete_modal.tsx
@@ -38,6 +38,18 @@ interface Dependencies {
export const getFieldDeleteModalOpener =
({ core, dataViews, usageCollection }: Dependencies) =>
(options: OpenFieldDeleteModalOptions): CloseEditor => {
+ if (typeof options.fieldName === 'string') {
+ const fieldToDelete = options.ctx.dataView.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/data_view_field_editor/public/open_editor.tsx b/src/plugins/data_view_field_editor/public/open_editor.tsx
index d3a91739dc94a..bb4bbd975fb75 100644
--- a/src/plugins/data_view_field_editor/public/open_editor.tsx
+++ b/src/plugins/data_view_field_editor/public/open_editor.tsx
@@ -17,12 +17,12 @@ import {
DataPublicPluginStart,
DataView,
UsageCollectionStart,
+ RuntimeType,
DataViewsPublicPluginStart,
FieldFormatsStart,
- 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';
@@ -31,7 +31,7 @@ export interface OpenFieldEditorOptions {
ctx: {
dataView: DataView;
};
- onSave?: (field: DataViewField) => void;
+ onSave?: (field: DataViewField[]) => void;
fieldName?: string;
}
@@ -85,7 +85,7 @@ export const getFieldEditorOpener =
}
};
- const onSaveField = (updatedField: DataViewField) => {
+ const onSaveField = (updatedField: DataViewField[]) => {
closeEditor();
if (onSave) {
@@ -93,9 +93,9 @@ export const getFieldEditorOpener =
}
};
- const field = fieldName ? dataView.getFieldByName(fieldName) : undefined;
+ const dataViewField = fieldName ? dataView.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 },
@@ -106,14 +106,41 @@ export const getFieldEditorOpener =
const isNewRuntimeField = !fieldName;
const isExistingRuntimeField =
- field &&
- field.runtimeField &&
- !field.isMapped &&
+ dataViewField &&
+ dataViewField.runtimeField &&
+ !dataViewField.isMapped &&
// treat composite field instances as mapped fields for field editing purposes
- field.runtimeField.type !== ('composite' as RuntimeType);
+ dataViewField.runtimeField.type !== ('composite' as RuntimeType);
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,
+ ...dataView.getRuntimeField(compositeName)!,
+ };
+ } else if (isExistingRuntimeField) {
+ // Runtime field
+ field = {
+ name: fieldName!,
+ ...dataView.getRuntimeField(fieldName!)!,
+ };
+ } else {
+ // Concrete field
+ field = {
+ name: fieldName!,
+ type: (dataViewField?.esTypes ? dataViewField.esTypes[0] : 'keyword') as RuntimeType,
+ customLabel: dataViewField.customLabel,
+ popularity: dataViewField.count,
+ format: dataView.getFormatterForFieldNoDefault(fieldName!)?.toJSON(),
+ };
+ }
+ }
overlayRef = overlays.openFlyout(
toMountPoint(
diff --git a/src/plugins/data_view_field_editor/public/shared_imports.ts b/src/plugins/data_view_field_editor/public/shared_imports.ts
index c8332a2afe76d..ba29fc642447e 100644
--- a/src/plugins/data_view_field_editor/public/shared_imports.ts
+++ b/src/plugins/data_view_field_editor/public/shared_imports.ts
@@ -13,12 +13,17 @@ export type { FieldFormatsStart } from '../../field_formats/public';
export type { UsageCollectionStart } from '../../usage_collection/public';
-export type { RuntimeType, RuntimeField } from '../../data/common';
+export type {
+ RuntimeType,
+ RuntimeField,
+ RuntimeFieldSpec,
+ RuntimeFieldSubField,
+} from '../../data_views/common';
export { KBN_FIELD_TYPES, ES_FIELD_TYPES } from '../../data/common';
export { createKibanaReactContext, toMountPoint, CodeEditor } from '../../kibana_react/public';
-export { FieldFormat } from '../../field_formats/common';
+export type { FieldFormat, SerializedFieldFormat } from '../../field_formats/common';
export type {
FormSchema,
diff --git a/src/plugins/data_view_field_editor/public/types.ts b/src/plugins/data_view_field_editor/public/types.ts
index 25f97e6737bf2..be623c6cc8776 100644
--- a/src/plugins/data_view_field_editor/public/types.ts
+++ b/src/plugins/data_view_field_editor/public/types.ts
@@ -12,7 +12,6 @@ import {
DataPublicPluginStart,
DataViewsPublicPluginStart,
RuntimeField,
- RuntimeType,
UsageCollectionStart,
FieldFormatsStart,
} from './shared_imports';
@@ -47,25 +46,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/data_view_field_editor/server/routes/field_preview.ts b/src/plugins/data_view_field_editor/server/routes/field_preview.ts
index 022cd92a4bb1e..14b865b61eaa0 100644
--- a/src/plugins/data_view_field_editor/server/routes/field_preview.ts
+++ b/src/plugins/data_view_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' }),
documentId: schema.string(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx
index 6a758d8deb9b0..b697d859f7ee4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx
@@ -16,7 +16,6 @@ import * as i18n from './translations';
const StyledButton = styled(EuiButton)`
margin-left: ${({ theme }) => theme.eui.paddingSizes.m};
`;
-
export interface UseCreateFieldButtonProps {
hasFieldEditPermission: boolean;
loading: boolean;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
index b38b6d03749c9..beb49d3cb0913 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
@@ -73,38 +73,41 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({
const closeFieldEditor = dataViewFieldEditor.openEditor({
ctx: { dataView },
fieldName,
- onSave: async (savedField: DataViewField) => {
+ onSave: async (savedFields: DataViewField[]) => {
// Fetch the updated list of fields
// Using cleanCache since the number of fields might have not changed, but we need to update the state anyway
await indexFieldsSearch({ dataViewId: selectedDataViewId, cleanCache: true });
- if (fieldName && fieldName !== savedField.name) {
- // Remove old field from event table when renaming a field
+ for (const savedField of savedFields) {
+ if (fieldName && fieldName !== savedField.name) {
+ // Remove old field from event table when renaming a field
+ dispatch(
+ removeColumn({
+ columnId: fieldName,
+ id: timelineId,
+ })
+ );
+ }
+
+ // Add the saved column field to the table in any case
dispatch(
- removeColumn({
- columnId: fieldName,
+ upsertColumn({
+ column: {
+ columnHeaderType: defaultColumnHeaderType,
+ id: savedField.name,
+ initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
+ },
id: timelineId,
+ index: 0,
})
);
}
-
- // Add the saved column field to the table in any case
- dispatch(
- upsertColumn({
- column: {
- columnHeaderType: defaultColumnHeaderType,
- id: savedField.name,
- initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
- },
- id: timelineId,
- index: 0,
- })
- );
if (editorActionsRef) {
editorActionsRef.current = null;
}
},
});
+
if (editorActionsRef) {
editorActionsRef.current = {
closeEditor: () => {
diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts
index 285f3879681c7..52f1485107c76 100644
--- a/x-pack/plugins/transform/common/api_schemas/common.ts
+++ b/x-pack/plugins/transform/common/api_schemas/common.ts
@@ -6,9 +6,7 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { i18n } from '@kbn/i18n';
import { TRANSFORM_STATE } from '../constants';
-import { isRuntimeField } from '../shared_imports';
export const transformIdsSchema = schema.arrayOf(
schema.object({
@@ -58,17 +56,27 @@ export interface CommonResponseStatusSchema {
}
export const runtimeMappingsSchema = schema.maybe(
- schema.object(
- {},
- {
- unknowns: 'allow',
- validate: (v: object) => {
- if (Object.values(v).some((o) => !isRuntimeField(o))) {
- return i18n.translate('xpack.transform.invalidRuntimeFieldMessage', {
- defaultMessage: 'Invalid runtime field',
- });
- }
- },
- }
+ schema.recordOf(
+ schema.string(),
+ schema.object({
+ type: schema.oneOf([
+ schema.literal('keyword'),
+ schema.literal('long'),
+ schema.literal('double'),
+ schema.literal('date'),
+ schema.literal('ip'),
+ schema.literal('boolean'),
+ schema.literal('geo_point'),
+ schema.literal('composite'),
+ ]),
+ script: schema.maybe(
+ schema.oneOf([
+ schema.string(),
+ schema.object({
+ source: schema.string(),
+ }),
+ ])
+ ),
+ })
)
);
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
);
diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts
index 2f82b9a70389b..4d6337d89e1c4 100644
--- a/x-pack/plugins/transform/server/routes/api/transforms.ts
+++ b/x-pack/plugins/transform/server/routes/api/transforms.ts
@@ -638,7 +638,11 @@ const previewTransformHandler: RequestHandler<
> = async (ctx, req, res) => {
try {
const reqBody = req.body;
+
const body = await ctx.core.elasticsearch.client.asCurrentUser.transform.previewTransform({
+ // todo examine this
+ // @ts-expect-error The ES client does not yet include the "composite" runtime type
+ // Once "composite" is added to the MappingRuntimeFieldType, those comments can be safely removed.
body: reqBody,
});
if (isLatestTransform(reqBody)) {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 9c09921142f0b..5cea09472cab2 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -27098,7 +27098,6 @@
"xpack.transform.home.breadcrumbTitle": "変換",
"xpack.transform.indexPreview.copyClipboardTooltip": "インデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。",
"xpack.transform.indexPreview.copyRuntimeFieldsClipboardTooltip": "ランタイムフィールドの開発コンソールステートメントをクリップボードにコピーします。",
- "xpack.transform.invalidRuntimeFieldMessage": "無効なランタイムフィールド",
"xpack.transform.latestPreview.latestPreviewIncompleteConfigCalloutBody": "1 つ以上の一意キーと並べ替えフィールドを選択してください。",
"xpack.transform.licenseCheckErrorMessage": "ライセンス確認失敗",
"xpack.transform.list.emptyPromptButtonText": "初めての変換を作成してみましょう。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2252df4d2d2ab..3e7a1636e1906 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -27129,7 +27129,6 @@
"xpack.transform.home.breadcrumbTitle": "转换",
"xpack.transform.indexPreview.copyClipboardTooltip": "将索引预览的开发控制台语句复制到剪贴板。",
"xpack.transform.indexPreview.copyRuntimeFieldsClipboardTooltip": "将运行时字段的开发控制台语句复制到剪贴板。",
- "xpack.transform.invalidRuntimeFieldMessage": "运行时字段无效",
"xpack.transform.latestPreview.latestPreviewIncompleteConfigCalloutBody": "请选择至少一个唯一键和排序字段。",
"xpack.transform.licenseCheckErrorMessage": "许可证检查失败",
"xpack.transform.list.emptyPromptButtonText": "创建您的首个转换",