mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Data views] Add pre-configuration options to runtime field editor fly-out (#136769)
* [Data views] Add pre-configuration options to runtime field editor fly-out * fix test * more polish * update example app functional test * fix functional test * improve comment * fix unexported public apis * comments for public apis * restrict runaway metrics changes * more comments for public api * fix fn test * revert updates to api_docs * more public api comments in data_view_field_editor * fix api comments * add public api export * clean up FieldFormatConfig types * cleanup * allow checkbox to be visually checked
This commit is contained in:
parent
b8f41a0eea
commit
0b8b66f73f
18 changed files with 244 additions and 116 deletions
|
@ -6,23 +6,28 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
DefaultItemAction,
|
||||
EuiButton,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiPage,
|
||||
EuiPageHeader,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiButton,
|
||||
EuiInMemoryTable,
|
||||
EuiPageHeader,
|
||||
EuiText,
|
||||
DefaultItemAction,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { AppMountParameters } from '@kbn/core/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import React, { useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
interface Props {
|
||||
dataView?: DataView;
|
||||
|
@ -33,6 +38,8 @@ const DataViewFieldEditorExample = ({ dataView, dataViewFieldEditor }: Props) =>
|
|||
const [fields, setFields] = useState<DataViewField[]>(
|
||||
dataView?.fields.getAll().filter((f) => !f.scripted) || []
|
||||
);
|
||||
const [preconfigured, setPreconfigured] = useState<boolean>(false);
|
||||
|
||||
const refreshFields = () => setFields(dataView?.fields.getAll().filter((f) => !f.scripted) || []);
|
||||
const columns = [
|
||||
{
|
||||
|
@ -75,22 +82,42 @@ const DataViewFieldEditorExample = ({ dataView, dataViewFieldEditor }: Props) =>
|
|||
},
|
||||
];
|
||||
|
||||
const preconfigureId = useGeneratedHtmlId({ prefix: 'usePreconfigured' });
|
||||
const content = dataView ? (
|
||||
<>
|
||||
<EuiText data-test-subj="dataViewTitle">Data view: {dataView.title}</EuiText>
|
||||
<div>
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
dataViewFieldEditor.openEditor({
|
||||
ctx: { dataView },
|
||||
onSave: refreshFields,
|
||||
})
|
||||
}
|
||||
data-test-subj="addField"
|
||||
>
|
||||
Add field
|
||||
</EuiButton>
|
||||
</div>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
dataViewFieldEditor.openEditor({
|
||||
ctx: { dataView },
|
||||
onSave: refreshFields,
|
||||
fieldToCreate: preconfigured
|
||||
? {
|
||||
name: 'demotestfield',
|
||||
type: 'boolean',
|
||||
script: { source: 'emit(true)' }, // optional
|
||||
customLabel: 'cool demo test field', // optional
|
||||
format: { id: 'boolean' }, // optional
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
data-test-subj="addField"
|
||||
>
|
||||
Add field
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj="preconfiguredControlWrapper">
|
||||
<EuiCheckbox
|
||||
id={preconfigureId}
|
||||
checked={preconfigured}
|
||||
label="Use preconfigured options"
|
||||
onChange={() => setPreconfigured(!preconfigured)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiInMemoryTable<DataViewField>
|
||||
items={fields}
|
||||
columns={columns}
|
||||
|
|
|
@ -98,7 +98,7 @@ describe('<FieldEditor />', () => {
|
|||
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")' },
|
||||
};
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ export const setup = async (props?: Partial<Props>, deps?: Partial<Context>) =>
|
|||
|
||||
// Setup testbed
|
||||
await act(async () => {
|
||||
testBed = await registerTestBed(WithFieldEditorDependencies(FieldEditorFlyoutContent, deps), {
|
||||
testBed = registerTestBed(WithFieldEditorDependencies(FieldEditorFlyoutContent, deps), {
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
},
|
||||
|
|
|
@ -36,16 +36,16 @@ describe('<FieldEditorFlyoutContent />', () => {
|
|||
expect(find('flyoutTitle').text()).toBe('Create field');
|
||||
});
|
||||
|
||||
test('should allow a field to be provided', async () => {
|
||||
test('should allow an existing field to be provided', async () => {
|
||||
const field = {
|
||||
name: 'foo',
|
||||
type: 'ip',
|
||||
type: 'ip' as const,
|
||||
script: {
|
||||
source: 'emit("hello world")',
|
||||
},
|
||||
};
|
||||
|
||||
const { find } = await setup({ field });
|
||||
const { find } = await setup({ fieldToEdit: field });
|
||||
|
||||
expect(find('flyoutTitle').text()).toBe(`Edit field 'foo'`);
|
||||
expect(find('nameField.input').props().value).toBe(field.name);
|
||||
|
@ -53,15 +53,32 @@ describe('<FieldEditorFlyoutContent />', () => {
|
|||
expect(find('scriptField').props().value).toBe(field.script.source);
|
||||
});
|
||||
|
||||
test('should allow a new field to be created with initial configuration', async () => {
|
||||
const fieldToCreate = {
|
||||
name: 'demotestfield',
|
||||
type: 'boolean' as const,
|
||||
script: { source: 'emit(true)' },
|
||||
customLabel: 'cool demo test field',
|
||||
format: { id: 'boolean' },
|
||||
};
|
||||
|
||||
const { find } = await setup({ fieldToCreate });
|
||||
|
||||
expect(find('flyoutTitle').text()).toBe(`Create field`);
|
||||
expect(find('nameField.input').props().value).toBe(fieldToCreate.name);
|
||||
expect(find('typeField').props().value).toBe(fieldToCreate.type);
|
||||
expect(find('scriptField').props().value).toBe(fieldToCreate.script.source);
|
||||
});
|
||||
|
||||
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<Props['onSave']> = jest.fn();
|
||||
|
||||
const { find, actions } = await setup({ onSave, field });
|
||||
const { find, actions } = await setup({ onSave, fieldToEdit: field });
|
||||
|
||||
await act(async () => {
|
||||
find('fieldSaveButton').simulate('click');
|
||||
|
|
|
@ -164,7 +164,7 @@ export const setup = async (props?: Partial<Props>, deps?: Partial<Context>) =>
|
|||
|
||||
// Setup testbed
|
||||
await act(async () => {
|
||||
testBed = await registerTestBed(WithFieldEditorDependencies(FieldEditorFlyoutContent, deps), {
|
||||
testBed = registerTestBed(WithFieldEditorDependencies(FieldEditorFlyoutContent, deps), {
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
},
|
||||
|
|
|
@ -216,7 +216,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")',
|
||||
},
|
||||
|
@ -225,7 +225,7 @@ describe('Field editor Preview panel', () => {
|
|||
// We open the editor with a field to edit the empty prompt should not be there
|
||||
// as we have a script and we'll load the preview.
|
||||
await act(async () => {
|
||||
testBed = await setup({ field });
|
||||
testBed = await setup({ fieldToEdit: field });
|
||||
});
|
||||
|
||||
const { exists, component } = testBed;
|
||||
|
@ -237,7 +237,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: {},
|
||||
|
@ -245,7 +245,7 @@ describe('Field editor Preview panel', () => {
|
|||
};
|
||||
|
||||
await act(async () => {
|
||||
testBed = await setup({ field });
|
||||
testBed = await setup({ fieldToEdit: field });
|
||||
});
|
||||
|
||||
const { exists, component } = testBed;
|
||||
|
|
|
@ -64,7 +64,7 @@ export interface FieldFormInternal extends Omit<Field, 'type' | 'internalType'>
|
|||
}
|
||||
|
||||
export interface Props {
|
||||
/** Optional field to edit */
|
||||
/** Optional field to edit or preselected field to create */
|
||||
field?: Field;
|
||||
/** Handler to receive state changes updates */
|
||||
onChange?: (state: FieldEditorFormState) => void;
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports';
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { ES_FIELD_TYPES, UseField, useFormContext, useFormData } 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 +44,7 @@ export const FormatField = () => {
|
|||
}, [type, getFields]);
|
||||
|
||||
return (
|
||||
<UseField<FieldFormatConfig | undefined> path="format">
|
||||
<UseField<SerializedFieldFormat | undefined> path="format">
|
||||
{({ setValue, errors, value }) => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -6,25 +6,24 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { Field } from '../types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { euiFlyoutClassname } from '../constants';
|
||||
import { FlyoutPanels } from './flyout_panels';
|
||||
import { useFieldEditorContext } from './field_editor_context';
|
||||
import { FieldEditor, FieldEditorFormState } from './field_editor/field_editor';
|
||||
import { FieldPreview, useFieldPreviewContext } from './preview';
|
||||
import type { Field } from '../types';
|
||||
import { ModifiedFieldModal, SaveFieldTypeOrNameChangedModal } from './confirm_modals';
|
||||
import { FieldEditor, FieldEditorFormState } from './field_editor/field_editor';
|
||||
import { useFieldEditorContext } from './field_editor_context';
|
||||
import { FlyoutPanels } from './flyout_panels';
|
||||
import { FieldPreview, useFieldPreviewContext } from './preview';
|
||||
|
||||
const i18nTexts = {
|
||||
cancelButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCancelButtonLabel', {
|
||||
|
@ -50,7 +49,9 @@ export interface Props {
|
|||
*/
|
||||
onCancel: () => void;
|
||||
/** Optional field to process */
|
||||
field?: Field;
|
||||
fieldToEdit?: Field;
|
||||
/** Optional preselected configuration for new field */
|
||||
fieldToCreate?: Field;
|
||||
isSavingField: boolean;
|
||||
/** Handler to call when the component mounts.
|
||||
* We will pass "up" data that the parent component might need
|
||||
|
@ -59,14 +60,15 @@ export interface Props {
|
|||
}
|
||||
|
||||
const FieldEditorFlyoutContentComponent = ({
|
||||
field,
|
||||
fieldToEdit,
|
||||
fieldToCreate,
|
||||
onSave,
|
||||
onCancel,
|
||||
isSavingField,
|
||||
onMounted,
|
||||
}: Props) => {
|
||||
const isMounted = useRef(false);
|
||||
const isEditingExistingField = !!field;
|
||||
const isEditingExistingField = !!fieldToEdit;
|
||||
const { dataView } = useFieldEditorContext();
|
||||
const {
|
||||
panel: { isVisible: isPanelVisible },
|
||||
|
@ -75,9 +77,9 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
const [formState, setFormState] = useState<FieldEditorFormState>({
|
||||
isSubmitted: false,
|
||||
isSubmitting: false,
|
||||
isValid: field ? true : undefined,
|
||||
submit: field
|
||||
? async () => ({ isValid: true, data: field })
|
||||
isValid: fieldToEdit ? true : undefined,
|
||||
submit: fieldToEdit
|
||||
? async () => ({ isValid: true, data: fieldToEdit })
|
||||
: async () => ({ isValid: false, data: {} as Field }),
|
||||
});
|
||||
|
||||
|
@ -106,8 +108,8 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
}
|
||||
|
||||
if (isValid) {
|
||||
const nameChange = field?.name !== data.name;
|
||||
const typeChange = field?.type !== data.type;
|
||||
const nameChange = fieldToEdit?.name !== data.name;
|
||||
const typeChange = fieldToEdit?.type !== data.type;
|
||||
|
||||
if (isEditingExistingField && (nameChange || typeChange)) {
|
||||
setModalVisibility({
|
||||
|
@ -118,7 +120,7 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
onSave(data);
|
||||
}
|
||||
}
|
||||
}, [onSave, submit, field, isEditingExistingField]);
|
||||
}, [onSave, submit, fieldToEdit, isEditingExistingField]);
|
||||
|
||||
const onClickCancel = useCallback(() => {
|
||||
const canClose = canCloseValidator();
|
||||
|
@ -132,7 +134,7 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
if (modalVisibility.confirmChangeNameOrType) {
|
||||
return (
|
||||
<SaveFieldTypeOrNameChangedModal
|
||||
fieldName={field?.name!}
|
||||
fieldName={fieldToEdit?.name!}
|
||||
onConfirm={async () => {
|
||||
const { data } = await submit();
|
||||
onSave(data);
|
||||
|
@ -196,12 +198,12 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
<FlyoutPanels.Header>
|
||||
<EuiTitle data-test-subj="flyoutTitle">
|
||||
<h2>
|
||||
{field ? (
|
||||
{fieldToEdit ? (
|
||||
<FormattedMessage
|
||||
id="indexPatternFieldEditor.editor.flyoutEditFieldTitle"
|
||||
defaultMessage="Edit field '{fieldName}'"
|
||||
values={{
|
||||
fieldName: field.name,
|
||||
fieldName: fieldToEdit.name,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -226,7 +228,7 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
</FlyoutPanels.Header>
|
||||
|
||||
<FieldEditor
|
||||
field={field}
|
||||
field={fieldToEdit ?? fieldToCreate}
|
||||
onChange={setFormState}
|
||||
onFormModifiedChange={setIsFormModified}
|
||||
/>
|
||||
|
|
|
@ -43,7 +43,9 @@ export interface Props {
|
|||
/** The Kibana field type of the field to create or edit (default: "runtime") */
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
/** Optional field to edit */
|
||||
field?: DataViewField;
|
||||
fieldToEdit?: DataViewField;
|
||||
/** Optional initial configuration for new field */
|
||||
fieldToCreate?: Field;
|
||||
/** Services */
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
notifications: NotificationsStart;
|
||||
|
@ -64,7 +66,8 @@ export interface Props {
|
|||
*/
|
||||
|
||||
export const FieldEditorFlyoutContentContainer = ({
|
||||
field,
|
||||
fieldToEdit,
|
||||
fieldToCreate,
|
||||
onSave,
|
||||
onCancel,
|
||||
onMounted,
|
||||
|
@ -80,7 +83,6 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
fieldFormats,
|
||||
uiSettings,
|
||||
}: Props) => {
|
||||
const fieldToEdit = deserializeField(dataView, field);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const { fields } = dataView;
|
||||
|
@ -92,7 +94,7 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
|
||||
fields
|
||||
.filter((fld) => {
|
||||
const isFieldBeingEdited = field?.name === fld.name;
|
||||
const isFieldBeingEdited = fieldToEdit?.name === fld.name;
|
||||
return !isFieldBeingEdited && fld.isMapped;
|
||||
})
|
||||
.forEach((fld) => {
|
||||
|
@ -103,7 +105,7 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
});
|
||||
|
||||
return existing;
|
||||
}, [fields, field]);
|
||||
}, [fields, fieldToEdit]);
|
||||
|
||||
const services = useMemo(
|
||||
() => ({
|
||||
|
@ -126,8 +128,8 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
// rename an existing runtime field
|
||||
if (field?.name && field.name !== updatedField.name) {
|
||||
dataView.removeRuntimeField(field.name);
|
||||
if (fieldToEdit?.name && fieldToEdit.name !== updatedField.name) {
|
||||
dataView.removeRuntimeField(fieldToEdit.name);
|
||||
}
|
||||
|
||||
dataView.addRuntimeField(updatedField.name, {
|
||||
|
@ -184,7 +186,15 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[onSave, dataView, dataViews, notifications, fieldTypeToProcess, field?.name, usageCollection]
|
||||
[
|
||||
onSave,
|
||||
dataView,
|
||||
dataViews,
|
||||
notifications,
|
||||
fieldTypeToProcess,
|
||||
fieldToEdit?.name,
|
||||
usageCollection,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -204,7 +214,8 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
onSave={saveField}
|
||||
onCancel={onCancel}
|
||||
onMounted={onMounted}
|
||||
field={fieldToEdit}
|
||||
fieldToCreate={fieldToCreate}
|
||||
fieldToEdit={deserializeField(dataView, fieldToEdit)}
|
||||
isSavingField={isSaving}
|
||||
/>
|
||||
</FieldPreviewProvider>
|
||||
|
|
|
@ -10,14 +10,17 @@ import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui';
|
|||
import { CoreStart } from '@kbn/core/public';
|
||||
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { FieldFormatInstanceType, FieldFormatParams } from '@kbn/field-formats-plugin/common';
|
||||
import type {
|
||||
FieldFormatInstanceType,
|
||||
FieldFormatParams,
|
||||
SerializedFieldFormat,
|
||||
} from '@kbn/field-formats-plugin/common';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FormatEditorServiceStart } from '../../service';
|
||||
import { FieldFormatConfig } from '../../types';
|
||||
import { FormatEditor } from './format_editor';
|
||||
|
||||
export interface FormatSelectEditorProps {
|
||||
|
@ -26,9 +29,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 {
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import type { RuntimeType, RuntimeField } from '../../shared_imports';
|
||||
import type { FieldFormatConfig, RuntimeFieldPainlessError } from '../../types';
|
||||
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import React from 'react';
|
||||
import type { RuntimeField, RuntimeType } from '../../shared_imports';
|
||||
import type { RuntimeFieldPainlessError } from '../../types';
|
||||
|
||||
export type From = 'cluster' | 'custom';
|
||||
|
||||
|
@ -54,7 +55,7 @@ export interface Params {
|
|||
index: string | null;
|
||||
type: RuntimeType | null;
|
||||
script: Required<RuntimeField>['script'] | null;
|
||||
format: FieldFormatConfig | null;
|
||||
format: SerializedFieldFormat | null;
|
||||
document: { [key: string]: unknown } | null;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
import { IndexPatternFieldEditorPlugin } from './plugin';
|
||||
|
||||
export type {
|
||||
Field,
|
||||
PluginSetup as IndexPatternFieldEditorSetup,
|
||||
PluginStart as IndexPatternFieldEditorStart,
|
||||
} from './types';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { DataViewField, DataView } from '../shared_imports';
|
||||
import { DataViewField, DataView, RuntimeType } from '../shared_imports';
|
||||
import type { Field, RuntimeFieldPainlessError } from '../types';
|
||||
|
||||
export const deserializeField = (dataView: DataView, field?: DataViewField): Field | undefined => {
|
||||
|
@ -16,7 +16,7 @@ export const deserializeField = (dataView: DataView, field?: DataViewField): Fie
|
|||
|
||||
return {
|
||||
name: field.name,
|
||||
type: field?.esTypes ? field.esTypes[0] : 'keyword',
|
||||
type: field?.esTypes ? (field.esTypes[0] as RuntimeType) : ('keyword' as const),
|
||||
script: field.runtimeField ? field.runtimeField.script : undefined,
|
||||
customLabel: field.customLabel,
|
||||
popularity: field.count,
|
||||
|
|
|
@ -6,33 +6,47 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CoreStart, OverlayRef } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
createKibanaReactContext,
|
||||
toMountPoint,
|
||||
DataViewField,
|
||||
import React from 'react';
|
||||
import { FieldEditorLoader } from './components/field_editor_loader';
|
||||
import { euiFlyoutClassname } from './constants';
|
||||
import type { ApiService } from './lib/api';
|
||||
import type {
|
||||
DataPublicPluginStart,
|
||||
DataView,
|
||||
UsageCollectionStart,
|
||||
DataViewField,
|
||||
DataViewsPublicPluginStart,
|
||||
FieldFormatsStart,
|
||||
RuntimeType,
|
||||
UsageCollectionStart,
|
||||
} from './shared_imports';
|
||||
import { createKibanaReactContext, toMountPoint } from './shared_imports';
|
||||
import type { CloseEditor, Field, InternalFieldType, PluginStart } from './types';
|
||||
|
||||
import type { PluginStart, InternalFieldType, CloseEditor } from './types';
|
||||
import type { ApiService } from './lib/api';
|
||||
import { euiFlyoutClassname } from './constants';
|
||||
import { FieldEditorLoader } from './components/field_editor_loader';
|
||||
|
||||
/**
|
||||
* Options for opening the field editor
|
||||
* @public
|
||||
*/
|
||||
export interface OpenFieldEditorOptions {
|
||||
/**
|
||||
* context containing the data view the field belongs to
|
||||
*/
|
||||
ctx: {
|
||||
dataView: DataView;
|
||||
};
|
||||
/**
|
||||
* action to take after field is saved
|
||||
*/
|
||||
onSave?: (field: DataViewField) => void;
|
||||
/**
|
||||
* field to edit, for existing field
|
||||
*/
|
||||
fieldName?: string;
|
||||
/**
|
||||
* pre-selectable options for new field creation
|
||||
*/
|
||||
fieldToCreate?: Field;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
|
@ -75,7 +89,8 @@ export const getFieldEditorOpener =
|
|||
|
||||
const openEditor = ({
|
||||
onSave,
|
||||
fieldName,
|
||||
fieldName: fieldNameToEdit,
|
||||
fieldToCreate,
|
||||
ctx: { dataView },
|
||||
}: OpenFieldEditorOptions): CloseEditor => {
|
||||
const closeEditor = () => {
|
||||
|
@ -93,24 +108,24 @@ export const getFieldEditorOpener =
|
|||
}
|
||||
};
|
||||
|
||||
const field = fieldName ? dataView.getFieldByName(fieldName) : undefined;
|
||||
const fieldToEdit = fieldNameToEdit ? dataView.getFieldByName(fieldNameToEdit) : undefined;
|
||||
|
||||
if (fieldName && !field) {
|
||||
if (fieldNameToEdit && !fieldToEdit) {
|
||||
const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', {
|
||||
defaultMessage: "Field named '{fieldName}' not found on index pattern",
|
||||
values: { fieldName },
|
||||
values: { fieldName: fieldNameToEdit },
|
||||
});
|
||||
notifications.toasts.addDanger(err);
|
||||
return closeEditor;
|
||||
}
|
||||
|
||||
const isNewRuntimeField = !fieldName;
|
||||
const isNewRuntimeField = !fieldNameToEdit;
|
||||
const isExistingRuntimeField =
|
||||
field &&
|
||||
field.runtimeField &&
|
||||
!field.isMapped &&
|
||||
fieldToEdit &&
|
||||
fieldToEdit.runtimeField &&
|
||||
!fieldToEdit.isMapped &&
|
||||
// treat composite field instances as mapped fields for field editing purposes
|
||||
field.runtimeField.type !== ('composite' as RuntimeType);
|
||||
fieldToEdit.runtimeField.type !== ('composite' as RuntimeType);
|
||||
const fieldTypeToProcess: InternalFieldType =
|
||||
isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete';
|
||||
|
||||
|
@ -122,7 +137,8 @@ export const getFieldEditorOpener =
|
|||
onCancel={closeEditor}
|
||||
onMounted={onMounted}
|
||||
docLinks={docLinks}
|
||||
field={field}
|
||||
fieldToEdit={fieldToEdit}
|
||||
fieldToCreate={fieldToCreate}
|
||||
fieldTypeToProcess={fieldTypeToProcess}
|
||||
dataView={dataView}
|
||||
search={search}
|
||||
|
@ -150,7 +166,7 @@ export const getFieldEditorOpener =
|
|||
: i18n.translate('indexPatternFieldEditor.editField.flyoutAriaLabel', {
|
||||
defaultMessage: 'Edit {fieldName} field',
|
||||
values: {
|
||||
fieldName,
|
||||
fieldName: fieldNameToEdit,
|
||||
},
|
||||
}),
|
||||
onClose: (flyout) => {
|
||||
|
|
|
@ -28,7 +28,7 @@ export class IndexPatternFieldEditorPlugin
|
|||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: StartPlugins) {
|
||||
public start(core: CoreStart, plugins: StartPlugins): PluginStart {
|
||||
const { fieldFormatEditors } = this.formatEditorService.start();
|
||||
const { http } = core;
|
||||
const { data, usageCollection, dataViews, fieldFormats } = plugins;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/public';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { DeleteFieldProviderProps } from './components';
|
||||
import { OpenFieldDeleteModalOptions } from './open_delete_modal';
|
||||
|
@ -21,11 +21,22 @@ import {
|
|||
UsageCollectionStart,
|
||||
} from './shared_imports';
|
||||
|
||||
/**
|
||||
* Public setup contract of data view field editor
|
||||
* @public
|
||||
*/
|
||||
export interface PluginSetup {
|
||||
fieldFormatEditors: FormatEditorServiceSetup['fieldFormatEditors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Public start contract of data view field editor
|
||||
* @public
|
||||
*/
|
||||
export interface PluginStart {
|
||||
/**
|
||||
* method to open the data view field editor fly-out
|
||||
*/
|
||||
openEditor(options: OpenFieldEditorOptions): () => void;
|
||||
openDeleteModal(options: OpenFieldDeleteModalOptions): () => void;
|
||||
fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors'];
|
||||
|
@ -47,18 +58,35 @@ export interface StartPlugins {
|
|||
|
||||
export type InternalFieldType = 'concrete' | 'runtime';
|
||||
|
||||
/**
|
||||
* The data model for the field editor
|
||||
* @public
|
||||
*/
|
||||
export interface Field {
|
||||
name: string;
|
||||
type: RuntimeField['type'] | string;
|
||||
/**
|
||||
* name / path used for the field
|
||||
*/
|
||||
name: FieldSpec['name'];
|
||||
/**
|
||||
* ES type
|
||||
*/
|
||||
type: RuntimeType;
|
||||
/**
|
||||
* source of the runtime field script
|
||||
*/
|
||||
script?: RuntimeField['script'];
|
||||
customLabel?: string;
|
||||
/**
|
||||
* custom label for display
|
||||
*/
|
||||
customLabel?: FieldSpec['customLabel'];
|
||||
/**
|
||||
* custom popularity
|
||||
*/
|
||||
popularity?: number;
|
||||
format?: FieldFormatConfig;
|
||||
}
|
||||
|
||||
export interface FieldFormatConfig {
|
||||
id: string;
|
||||
params?: SerializableRecord;
|
||||
/**
|
||||
* configuration of the field format
|
||||
*/
|
||||
format?: FieldSpec['format'];
|
||||
}
|
||||
|
||||
export interface EsRuntimeField {
|
||||
|
|
|
@ -6,19 +6,41 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { PluginFunctionalProviderContext } from '../../plugin_functional/services';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: PluginFunctionalProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
|
||||
describe('', () => {
|
||||
it('finds a data view', async () => {
|
||||
await testSubjects.existOrFail('dataViewTitle');
|
||||
});
|
||||
|
||||
it('opens the field editor', async () => {
|
||||
await testSubjects.click('addField');
|
||||
await testSubjects.existOrFail('flyoutTitle');
|
||||
await testSubjects.click('closeFlyoutButton');
|
||||
});
|
||||
|
||||
it('uses preconfigured options for a new field', async () => {
|
||||
// find the checkbox label and click it - `testSubjects.setCheckbox()` is not working for our checkbox
|
||||
const controlWrapper = await testSubjects.find('preconfiguredControlWrapper');
|
||||
const control = await find.descendantDisplayedByCssSelector('label', controlWrapper);
|
||||
await control.click();
|
||||
|
||||
await testSubjects.click('addField');
|
||||
await testSubjects.existOrFail('flyoutTitle');
|
||||
|
||||
const nameField = await testSubjects.find('nameField');
|
||||
const nameInput = await find.descendantDisplayedByCssSelector(
|
||||
'[data-test-subj=input]',
|
||||
nameField
|
||||
);
|
||||
|
||||
expect(await nameInput.getAttribute('value')).to.equal('demotestfield');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue