From a4f851fff14bced704df845bf598d3516775fcdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Thu, 22 Oct 2020 14:24:30 +0200 Subject: [PATCH 1/6] Add kibana.json plugin definition --- x-pack/plugins/runtime_fields/kibana.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 x-pack/plugins/runtime_fields/kibana.json diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_fields/kibana.json new file mode 100644 index 0000000000000..05d806bc85e7f --- /dev/null +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "runtimeFields", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [ + ], + "optionalPlugins": [ + ], + "configPath": ["xpack", "runtime_fields"], + "requiredBundles": [ + "esUiShared" + ] +} From cf7bf911da92b850bb4c01799e6b1232a3a9488e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 30 Oct 2020 09:45:07 +0100 Subject: [PATCH 2/6] [Runtime fields editor] Form UI (#81766) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + .../hook_form_lib/components/use_field.tsx | 2 +- x-pack/.i18nrc.json | 3 +- x-pack/plugins/index_management/kibana.json | 3 +- .../runtime_type_parameter.tsx | 19 ++- .../constants/field_options.tsx | 29 ---- .../constants/parameters_definition.tsx | 4 +- .../mappings_editor/shared_imports.ts | 2 + .../mappings_editor/types/document_fields.ts | 4 +- x-pack/plugins/runtime_fields/kibana.json | 1 + .../public/__jest__/setup_environment.tsx | 44 ++++++ .../runtime_fields/public/components/index.ts | 7 + .../components/runtime_field_form/index.ts | 7 + .../runtime_field_form.test.tsx | 89 +++++++++++ .../runtime_field_form/runtime_field_form.tsx | 148 ++++++++++++++++++ .../components/runtime_field_form/schema.ts | 58 +++++++ .../runtime_fields/public/constants.ts | 37 +++++ x-pack/plugins/runtime_fields/public/index.ts | 14 ++ .../plugins/runtime_fields/public/plugin.ts | 23 +++ .../runtime_fields/public/shared_imports.ts | 19 +++ .../runtime_fields/public/test_utils.ts | 7 + x-pack/plugins/runtime_fields/public/types.ts | 34 ++++ 23 files changed, 517 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts create mode 100644 x-pack/plugins/runtime_fields/public/constants.ts create mode 100644 x-pack/plugins/runtime_fields/public/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/plugin.ts create mode 100644 x-pack/plugins/runtime_fields/public/shared_imports.ts create mode 100644 x-pack/plugins/runtime_fields/public/test_utils.ts create mode 100644 x-pack/plugins/runtime_fields/public/types.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 8e08c3806446d..4000d3e70d646 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -460,6 +460,10 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. +|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields[runtimeFields] +|WARNING: Missing README. + + |{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] |WARNING: Missing README. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c660d37222504..6bbec0786f4bc 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -98,3 +98,4 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 + runtimeFields: 26275 diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index a3a0984d4a736..4024eea008588 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -31,7 +31,7 @@ export interface Props { componentProps?: Record; readDefaultValueOnForm?: boolean; onChange?: (value: I) => void; - children?: (field: FieldHook) => JSX.Element; + children?: (field: FieldHook) => JSX.Element | null; [key: string]: any; } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8993213d91f23..e855a13d20ca5 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -36,11 +36,12 @@ "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": ["plugins/maps"], "xpack.ml": ["plugins/ml"], + "xpack.painlessLab": "plugins/painless_lab", "xpack.monitoring": ["plugins/monitoring"], "xpack.remoteClusters": "plugins/remote_clusters", - "xpack.painlessLab": "plugins/painless_lab", "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["plugins/rollup"], + "xpack.runtimeFields": "plugins/runtime_fields", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 28846414ca2e8..097ac03aabd22 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -17,6 +17,7 @@ "configPath": ["xpack", "index_management"], "requiredBundles": [ "kibanaReact", - "esUiShared" + "esUiShared", + "runtimeFields" ] } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx index 4bdb15af5e7d9..95a6c5364ac4d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/runtime_type_parameter.tsx @@ -14,10 +14,10 @@ import { EuiSpacer, } from '@elastic/eui'; -import { UseField } from '../../../shared_imports'; +import { UseField, RUNTIME_FIELD_OPTIONS } from '../../../shared_imports'; import { DataType } from '../../../types'; import { getFieldConfig } from '../../../lib'; -import { RUNTIME_FIELD_OPTIONS, TYPE_DEFINITION } from '../../../constants'; +import { TYPE_DEFINITION } from '../../../constants'; import { EditFieldFormRow, FieldDescriptionSection } from '../fields/edit_field'; interface Props { @@ -26,7 +26,10 @@ interface Props { export const RuntimeTypeParameter = ({ stack }: Props) => { return ( - + + path="runtime_type" + config={getFieldConfig('runtime_type')} + > {(runtimeTypeField) => { const { label, value, setValue } = runtimeTypeField; const typeDefinition = @@ -44,8 +47,14 @@ export const RuntimeTypeParameter = ({ stack }: Props) => { )} singleSelection={{ asPlainText: true }} options={RUNTIME_FIELD_OPTIONS} - selectedOptions={value as EuiComboBoxOptionOption[]} - onChange={setValue} + selectedOptions={value} + onChange={(newValue) => { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} isClearable={false} fullWidth /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx index 25fdac5089b86..46292b7b2d357 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx @@ -28,35 +28,6 @@ export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map }) ) as ComboBoxOption[]; -export const RUNTIME_FIELD_OPTIONS = [ - { - label: 'Keyword', - value: 'keyword', - }, - { - label: 'Long', - value: 'long', - }, - { - label: 'Double', - value: 'double', - }, - { - label: 'Date', - value: 'date', - }, - { - label: 'IP', - value: 'ip', - }, - { - label: 'Boolean', - value: 'boolean', - }, -] as ComboBoxOption[]; - -export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; - interface SuperSelectOptionConfig { inputDisplay: string; dropdownDisplay: JSX.Element; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index 1434b7d4b4429..64f84ee2611a0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -16,11 +16,12 @@ import { ValidationFuncArg, fieldFormatters, FieldConfig, + RUNTIME_FIELD_OPTIONS, + RuntimeType, } from '../shared_imports'; import { AliasOption, DataType, - RuntimeType, ComboBoxOption, ParameterName, ParameterDefinition, @@ -28,7 +29,6 @@ import { import { documentationService } from '../../../services/documentation'; import { INDEX_DEFAULT } from './default_values'; import { TYPE_DEFINITION } from './data_types_definition'; -import { RUNTIME_FIELD_OPTIONS } from './field_options'; const { toInt } = fieldFormatters; const { emptyField, containsCharsField, numberGreaterThanField, isJsonField } = fieldValidators; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 54b2486108183..68b40e876f655 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -53,3 +53,5 @@ export { } from '../../../../../../../src/plugins/es_ui_shared/public'; export { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; + +export { RUNTIME_FIELD_OPTIONS, RuntimeType } from '../../../../../runtime_fields/public'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index ee4dd55a5801f..b143eedd4f9d4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -8,7 +8,7 @@ import { ReactNode } from 'react'; import { GenericObject } from './mappings_editor'; import { FieldConfig } from '../shared_imports'; -import { PARAMETERS_DEFINITION, RUNTIME_FIELD_TYPES } from '../constants'; +import { PARAMETERS_DEFINITION } from '../constants'; export interface DataTypeDefinition { label: string; @@ -76,8 +76,6 @@ export type SubType = NumericType | RangeType; export type DataType = MainType | SubType; -export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; - export type NumericType = | 'long' | 'integer' diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_fields/kibana.json index 05d806bc85e7f..65932c723c474 100644 --- a/x-pack/plugins/runtime_fields/kibana.json +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -9,6 +9,7 @@ ], "configPath": ["xpack", "runtime_fields"], "requiredBundles": [ + "kibanaReact", "esUiShared" ] } diff --git a/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx new file mode 100644 index 0000000000000..4453556621077 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +jest.mock('../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../src/plugins/kibana_react/public'); + + const CodeEditorMock = (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ); + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); diff --git a/x-pack/plugins/runtime_fields/public/components/index.ts b/x-pack/plugins/runtime_fields/public/components/index.ts new file mode 100644 index 0000000000000..461974a870446 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldForm } from './runtime_field_form'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts new file mode 100644 index 0000000000000..461974a870446 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldForm } from './runtime_field_form'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx new file mode 100644 index 0000000000000..e380e0b44aac5 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import '../../__jest__/setup_environment'; +import { registerTestBed, TestBed } from '../../test_utils'; +import { RuntimeField } from '../../types'; +import { RuntimeFieldForm, Props, FormState } from './runtime_field_form'; + +const setup = (props?: Props) => + registerTestBed(RuntimeFieldForm, { + memoryRouter: { + wrapComponent: false, + }, + })(props) as TestBed; + +const docsBaseUri = 'https://jestTest.elastic.co'; + +describe('Runtime field form', () => { + let testBed: TestBed; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FormState[] => onChange.mock.calls[onChange.mock.calls.length - 1]; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('should render expected 3 fields (name, returnType, script)', () => { + testBed = setup({ docsBaseUri }); + const { exists } = testBed; + + expect(exists('nameField')).toBe(true); + expect(exists('typeField')).toBe(true); + expect(exists('scriptField')).toBe(true); + }); + + test('should have a link to learn more about painless syntax', () => { + testBed = setup({ docsBaseUri }); + const { exists, find } = testBed; + + expect(exists('painlessSyntaxLearnMoreLink')).toBe(true); + expect(find('painlessSyntaxLearnMoreLink').props().href).toContain(docsBaseUri); + }); + + test('should accept a "defaultValue" prop', () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ defaultValue, docsBaseUri }); + const { find } = testBed; + + expect(find('nameField.input').props().value).toBe(defaultValue.name); + expect(find('typeField').props().value).toBe(defaultValue.type); + expect(find('scriptField').props().value).toBe(defaultValue.script); + }); + + test('should accept an "onChange" prop to forward the form state', async () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ onChange, defaultValue, docsBaseUri }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + let data; + await act(async () => { + ({ data } = await lastState.submit()); + }); + expect(data).toEqual(defaultValue); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx new file mode 100644 index 0000000000000..77ba4b81d65ee --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, + EuiLink, +} from '@elastic/eui'; + +import { useForm, Form, FormHook, UseField, TextField, CodeEditor } from '../../shared_imports'; +import { RuntimeField } from '../../types'; +import { RUNTIME_FIELD_OPTIONS } from '../../constants'; + +import { schema } from './schema'; + +export interface FormState { + isValid: boolean | undefined; + isSubmitted: boolean; + submit: FormHook['submit']; +} + +export interface Props { + docsBaseUri: string; + defaultValue?: RuntimeField; + onChange?: (state: FormState) => void; +} + +const RuntimeFieldFormComp = ({ defaultValue, onChange, docsBaseUri }: Props) => { + const { form } = useForm({ defaultValue, schema }); + const { submit, isValid: isFormValid, isSubmitted } = form; + + useEffect(() => { + if (onChange) { + onChange({ isValid: isFormValid, isSubmitted, submit }); + } + }, [onChange, isFormValid, isSubmitted, submit]); + + return ( +
+ + {/* Name */} + + + + + {/* Return type */} + + path="type"> + {({ label, value, setValue }) => { + if (value === undefined) { + return null; + } + return ( + <> + + { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} + isClearable={false} + data-test-subj="typeField" + fullWidth + /> + + + ); + }} + + + + + + + {/* Script */} + path="script"> + {({ value, setValue, label, helpText, isValid, getErrorsMessages }) => { + return ( + + + + {i18n.translate('xpack.runtimeFields.form.script.learnMoreLinkText', { + defaultMessage: 'Learn more about syntax.', + })} + + + + } + fullWidth + > + + + ); + }} + + + ); +}; + +export const RuntimeFieldForm = React.memo(RuntimeFieldFormComp); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts new file mode 100644 index 0000000000000..abb7cf812200f --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +import { FormSchema, fieldValidators } from '../../shared_imports'; +import { RUNTIME_FIELD_OPTIONS } from '../../constants'; +import { RuntimeField, RuntimeType, ComboBoxOption } from '../../types'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + name: { + label: i18n.translate('xpack.runtimeFields.form.nameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.runtimeFields.form.validations.nameIsRequiredErrorMessage', { + defaultMessage: 'Give a name to the field.', + }) + ), + }, + ], + }, + type: { + label: i18n.translate('xpack.runtimeFields.form.runtimeTypeLabel', { + defaultMessage: 'Type', + }), + defaultValue: 'keyword', + deserializer: (fieldType?: RuntimeType) => { + if (!fieldType) { + return []; + } + + const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === fieldType)?.label; + return [{ label: label ?? fieldType, value: fieldType }]; + }, + serializer: (value: Array>) => value[0].value!, + }, + script: { + label: i18n.translate('xpack.runtimeFields.form.defineFieldLabel', { + defaultMessage: 'Define field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.runtimeFields.form.validations.scriptIsRequiredErrorMessage', { + defaultMessage: 'Script must emit() a value.', + }) + ), + }, + ], + }, +}; diff --git a/x-pack/plugins/runtime_fields/public/constants.ts b/x-pack/plugins/runtime_fields/public/constants.ts new file mode 100644 index 0000000000000..017b58c246afe --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ComboBoxOption } from './types'; + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; + +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export const RUNTIME_FIELD_OPTIONS: Array> = [ + { + label: 'Keyword', + value: 'keyword', + }, + { + label: 'Long', + value: 'long', + }, + { + label: 'Double', + value: 'double', + }, + { + label: 'Date', + value: 'date', + }, + { + label: 'IP', + value: 'ip', + }, + { + label: 'Boolean', + value: 'boolean', + }, +]; diff --git a/x-pack/plugins/runtime_fields/public/index.ts b/x-pack/plugins/runtime_fields/public/index.ts new file mode 100644 index 0000000000000..166cc0e734468 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RuntimeFieldsPlugin } from './plugin'; + +export { RuntimeFieldForm } from './components'; +export { RUNTIME_FIELD_OPTIONS } from './constants'; +export { RuntimeField, RuntimeType } from './types'; + +export function plugin() { + return new RuntimeFieldsPlugin(); +} diff --git a/x-pack/plugins/runtime_fields/public/plugin.ts b/x-pack/plugins/runtime_fields/public/plugin.ts new file mode 100644 index 0000000000000..d893a1e181811 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/plugin.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; + +export class RuntimeFieldsPlugin + implements Plugin { + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { + return {}; + } + + public start(core: CoreStart, plugins: StartPlugins) { + return {}; + } + + public stop() { + return {}; + } +} diff --git a/x-pack/plugins/runtime_fields/public/shared_imports.ts b/x-pack/plugins/runtime_fields/public/shared_imports.ts new file mode 100644 index 0000000000000..8ce22a66b627b --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/shared_imports.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + useForm, + Form, + FormSchema, + UseField, + FormHook, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { TextField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { CodeEditor } from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/runtime_fields/public/test_utils.ts b/x-pack/plugins/runtime_fields/public/test_utils.ts new file mode 100644 index 0000000000000..fb6c6cf2a79a5 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/test_utils.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerTestBed, TestBed } from '../../../test_utils/testbed'; diff --git a/x-pack/plugins/runtime_fields/public/types.ts b/x-pack/plugins/runtime_fields/public/types.ts new file mode 100644 index 0000000000000..9d1daa9eacb0e --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DataPublicPluginStart } from 'src/plugins/data/public'; + +import { RUNTIME_FIELD_TYPES } from './constants'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SetupPlugins {} + +export interface StartPlugins { + data: DataPublicPluginStart; +} + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export interface RuntimeField { + name: string; + type: RuntimeType; + script: string; +} + +export interface ComboBoxOption { + label: string; + value?: T; +} From 00faeb9633980d6374bd72f41dcbfb183b686129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 3 Nov 2020 17:48:44 +0100 Subject: [PATCH 3/6] [Runtime fields editor] Expose editor for consuming apps (#82116) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 4 +- packages/kbn-optimizer/limits.yml | 2 +- .../painless_script_parameter.tsx | 10 +- x-pack/plugins/runtime_fields/README.md | 136 ++++++++++++++++ .../public/__jest__/setup_environment.tsx | 4 +- .../runtime_fields/public/components/index.ts | 6 +- .../components/runtime_field_editor/index.ts | 7 + .../runtime_field_editor.test.tsx | 71 +++++++++ .../runtime_field_editor.tsx | 24 +++ .../index.ts | 7 + ...ntime_field_editor_flyout_content.test.tsx | 146 ++++++++++++++++++ .../runtime_field_editor_flyout_content.tsx | 146 ++++++++++++++++++ .../components/runtime_field_form/index.ts | 2 +- .../runtime_field_form.test.tsx | 14 +- .../runtime_field_form/runtime_field_form.tsx | 19 +-- x-pack/plugins/runtime_fields/public/index.ts | 6 +- .../public/lib/documentation.ts | 16 ++ .../runtime_fields/public/lib/index.ts | 7 + 18 files changed, 599 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/runtime_fields/README.md create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx create mode 100644 x-pack/plugins/runtime_fields/public/lib/documentation.ts create mode 100644 x-pack/plugins/runtime_fields/public/lib/index.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index a497bbd518e61..aed2e00dfeb6a 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -466,8 +466,8 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. -|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields[runtimeFields] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields/README.md[runtimeFields] +|Welcome to the home of the runtime field editor and everything related to runtime fields! |{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler] diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 2650a1cbb8136..64805d8831dbb 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -98,4 +98,4 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 - runtimeFields: 26275 + runtimeFields: 41752 diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx index 19746034b530c..9042e7f6ee328 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { PainlessLang } from '@kbn/monaco'; import { EuiFormRow, EuiDescribedFormGroup } from '@elastic/eui'; import { CodeEditor, UseField } from '../../../shared_imports'; @@ -18,7 +19,7 @@ interface Props { export const PainlessScriptParameter = ({ stack }: Props) => { return ( - + path="script.source" config={getFieldConfig('script')}> {(scriptField) => { const error = scriptField.getErrorsMessages(); const isInvalid = error ? Boolean(error.length) : false; @@ -26,11 +27,10 @@ export const PainlessScriptParameter = ({ stack }: Props) => { const field = ( ` +* As a standalone component that you can inline anywhere + +### Content of a `` + +```js +import React, { useState } from 'react'; +import { EuiFlyoutBody, EuiButton } from '@elastic/eui'; +import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields/public'; + +const MyComponent = () => { + const { docLinksStart } = useCoreContext(); // access the core start service + const [isFlyoutVisilbe, setIsFlyoutVisible] = useState(false); + + const saveRuntimeField = useCallback((field: RuntimeField) => { + // Do something with the field + }, []); + + return ( + <> + setIsFlyoutVisible(true)}>Create field + + {isFlyoutVisible && ( + setIsFlyoutVisible(false)}> + setIsFlyoutVisible(false)} + docLinks={docLinksStart} + defaultValue={/*optional runtime field to edit*/} + /> + + )} + + ) +} +``` + +#### With the `core.overlays.openFlyout` + +As an alternative you can open the flyout with the `core.overlays.openFlyout`. In this case you will need to wrap the editor with the `Provider` from the "kibana_react" plugin as it is a required dependency for the `` component. + +```js +import React, { useRef } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { OverlayRef } from 'src/core/public'; + +import { createKibanaReactContext, toMountPoint } from '../../src/plugins/kibana_react/public'; +import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields/public'; + +const MyComponent = () => { + // Access the core start service + const { docLinksStart, overlays, uiSettings } = useCoreContext(); + const flyoutEditor = useRef(null); + + const { openFlyout } = overlays; + + const saveRuntimeField = useCallback((field: RuntimeField) => { + // Do something with the field + }, []); + + const openRuntimeFieldEditor = useCallback(() => { + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings }); + + flyoutEditor.current = openFlyout( + toMountPoint( + + flyoutEditor.current?.close()} + docLinks={docLinksStart} + defaultValue={defaultRuntimeField} + /> + + ) + ); + }, [openFlyout, saveRuntimeField, uiSettings]); + + return ( + <> + Create field + + ) +} +``` + +### Standalone component + +```js +import React, { useState } from 'react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import { RuntimeFieldEditor, RuntimeField, RuntimeFieldFormState } from '../runtime_fields/public'; + +const MyComponent = () => { + const { docLinksStart } = useCoreContext(); // access the core start service + const [runtimeFieldFormState, setRuntimeFieldFormState] = useState({ + isSubmitted: false, + isValid: undefined, + submit: async() => Promise.resolve({ isValid: false, data: {} as RuntimeField }) + }); + + const { submit, isValid: isFormValid, isSubmitted } = runtimeFieldFormState; + + const saveRuntimeField = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + // Do something with the field (data) + } + }, [submit]); + + return ( + <> + + + + + + Save field + + + ) +} +``` \ No newline at end of file diff --git a/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx index 4453556621077..ccfe426cfdb09 100644 --- a/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx +++ b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx @@ -13,8 +13,8 @@ jest.mock('../../../../../src/plugins/kibana_react/public', () => { data-test-subj={props['data-test-subj'] || 'mockCodeEditor'} data-value={props.value} value={props.value} - onChange={(syntheticEvent: any) => { - props.onChange([syntheticEvent['0']]); + onChange={(e: React.ChangeEvent) => { + props.onChange(e.target.value); }} /> ); diff --git a/x-pack/plugins/runtime_fields/public/components/index.ts b/x-pack/plugins/runtime_fields/public/components/index.ts index 461974a870446..86ac968d39f21 100644 --- a/x-pack/plugins/runtime_fields/public/components/index.ts +++ b/x-pack/plugins/runtime_fields/public/components/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { RuntimeFieldForm } from './runtime_field_form'; +export { RuntimeFieldForm, FormState as RuntimeFieldFormState } from './runtime_field_form'; + +export { RuntimeFieldEditor } from './runtime_field_editor'; + +export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts new file mode 100644 index 0000000000000..62fa0bf991542 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldEditor } from './runtime_field_editor'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx new file mode 100644 index 0000000000000..c56bc16c304ad --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; +import { DocLinksStart } from 'src/core/public'; + +import '../../__jest__/setup_environment'; +import { registerTestBed, TestBed } from '../../test_utils'; +import { RuntimeField } from '../../types'; +import { RuntimeFieldForm, FormState } from '../runtime_field_form/runtime_field_form'; +import { RuntimeFieldEditor, Props } from './runtime_field_editor'; + +const setup = (props?: Props) => + registerTestBed(RuntimeFieldEditor, { + memoryRouter: { + wrapComponent: false, + }, + })(props) as TestBed; + +const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'https://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +describe('Runtime field editor', () => { + let testBed: TestBed; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FormState[] => onChange.mock.calls[onChange.mock.calls.length - 1]; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('should render the ', () => { + testBed = setup({ docLinks }); + const { component } = testBed; + + expect(component.find(RuntimeFieldForm).length).toBe(1); + }); + + test('should accept a defaultValue and onChange prop to forward the form state', async () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ onChange, defaultValue, docLinks }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + let data; + await act(async () => { + ({ data } = await lastState.submit()); + }); + expect(data).toEqual(defaultValue); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx new file mode 100644 index 0000000000000..07935be171fd2 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { DocLinksStart } from 'src/core/public'; + +import { RuntimeField } from '../../types'; +import { getLinks } from '../../lib'; +import { RuntimeFieldForm, Props as FormProps } from '../runtime_field_form/runtime_field_form'; + +export interface Props { + docLinks: DocLinksStart; + defaultValue?: RuntimeField; + onChange?: FormProps['onChange']; +} + +export const RuntimeFieldEditor = ({ defaultValue, onChange, docLinks }: Props) => { + const links = getLinks(docLinks); + + return ; +}; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts new file mode 100644 index 0000000000000..32234bfcc5600 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx new file mode 100644 index 0000000000000..8e47472295f45 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; +import { DocLinksStart } from 'src/core/public'; + +import '../../__jest__/setup_environment'; +import { registerTestBed, TestBed } from '../../test_utils'; +import { RuntimeField } from '../../types'; +import { RuntimeFieldEditorFlyoutContent, Props } from './runtime_field_editor_flyout_content'; + +const setup = (props?: Props) => + registerTestBed(RuntimeFieldEditorFlyoutContent, { + memoryRouter: { + wrapComponent: false, + }, + })(props) as TestBed; + +const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +const noop = () => {}; +const defaultProps = { onSave: noop, onCancel: noop, docLinks }; + +describe('Runtime field editor flyout', () => { + test('should have a flyout title', () => { + const { exists, find } = setup(defaultProps); + + expect(exists('flyoutTitle')).toBe(true); + expect(find('flyoutTitle').text()).toBe('Create new field'); + }); + + test('should allow a runtime field to be provided', () => { + const field: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + + const { find } = setup({ ...defaultProps, defaultValue: field }); + + expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`); + expect(find('nameField.input').props().value).toBe(field.name); + expect(find('typeField').props().value).toBe(field.type); + expect(find('scriptField').props().value).toBe(field.script); + }); + + test('should accept an onSave prop', async () => { + const field: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + const onSave: jest.Mock = jest.fn(); + + const { find } = setup({ ...defaultProps, onSave, defaultValue: field }); + + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + const fieldReturned: RuntimeField = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual(field); + }); + + test('should accept an onCancel prop', () => { + const onCancel = jest.fn(); + const { find } = setup({ ...defaultProps, onCancel }); + + find('closeFlyoutButton').simulate('click'); + + expect(onCancel).toHaveBeenCalled(); + }); + + describe('validation', () => { + test('should validate the fields and prevent saving invalid form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, exists, form, component } = setup({ ...defaultProps, onSave }); + + expect(find('saveFieldButton').props().disabled).toBe(false); + + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + component.update(); + + expect(onSave).toHaveBeenCalledTimes(0); + expect(find('saveFieldButton').props().disabled).toBe(true); + expect(form.getErrorsMessages()).toEqual([ + 'Give a name to the field.', + 'Script must emit() a value.', + ]); + expect(exists('formError')).toBe(true); + expect(find('formError').text()).toBe('Fix errors in form before continuing.'); + }); + + test('should forward values from the form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, form } = setup({ ...defaultProps, onSave }); + + act(() => { + form.setInputValue('nameField.input', 'someName'); + form.setInputValue('scriptField', 'script=123'); + }); + + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + let fieldReturned: RuntimeField = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', // default to keyword + script: 'script=123', + }); + + // Change the type and make sure it is forwarded + act(() => { + find('typeField').simulate('change', [ + { + label: 'Other type', + value: 'other_type', + }, + ]); + }); + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'other_type', + script: 'script=123', + }); + }); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx new file mode 100644 index 0000000000000..c7454cff0eb15 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; +import { DocLinksStart } from 'src/core/public'; + +import { RuntimeField } from '../../types'; +import { FormState } from '../runtime_field_form'; +import { RuntimeFieldEditor } from '../runtime_field_editor'; + +const geti18nTexts = (field?: RuntimeField) => { + return { + flyoutTitle: field + ? i18n.translate('xpack.runtimeFields.editor.flyoutEditFieldTitle', { + defaultMessage: 'Edit {fieldName} field', + values: { + fieldName: field.name, + }, + }) + : i18n.translate('xpack.runtimeFields.editor.flyoutDefaultTitle', { + defaultMessage: 'Create new field', + }), + closeButtonLabel: i18n.translate('xpack.runtimeFields.editor.flyoutCloseButtonLabel', { + defaultMessage: 'Close', + }), + saveButtonLabel: i18n.translate('xpack.runtimeFields.editor.flyoutSaveButtonLabel', { + defaultMessage: 'Save', + }), + formErrorsCalloutTitle: i18n.translate('xpack.runtimeFields.editor.validationErrorTitle', { + defaultMessage: 'Fix errors in form before continuing.', + }), + }; +}; + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: RuntimeField) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * An optional runtime field to edit + */ + defaultValue?: RuntimeField; +} + +export const RuntimeFieldEditorFlyoutContent = ({ + onSave, + onCancel, + docLinks, + defaultValue: field, +}: Props) => { + const i18nTexts = geti18nTexts(field); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isValid: field ? true : undefined, + submit: field + ? async () => ({ isValid: true, data: field }) + : async () => ({ isValid: false, data: {} as RuntimeField }), + }); + const { submit, isValid: isFormValid, isSubmitted } = formState; + + const onSaveField = useCallback(async () => { + const { isValid, data } = await submit(); + + if (isValid) { + onSave(data); + } + }, [submit, onSave]); + + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + + + + + + {isSubmitted && !isFormValid && ( + <> + + + + )} + + + + onCancel()} + data-test-subj="closeFlyoutButton" + > + {i18nTexts.closeButtonLabel} + + + + + onSaveField()} + data-test-subj="saveFieldButton" + disabled={isSubmitted && !isFormValid} + fill + > + {i18nTexts.saveButtonLabel} + + + + + + ); +}; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts index 461974a870446..4041a04aec4d1 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { RuntimeFieldForm } from './runtime_field_form'; +export { RuntimeFieldForm, FormState } from './runtime_field_form'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx index e380e0b44aac5..1829514856eed 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx @@ -17,7 +17,9 @@ const setup = (props?: Props) => }, })(props) as TestBed; -const docsBaseUri = 'https://jestTest.elastic.co'; +const links = { + painlessSyntax: 'https://jestTest.elastic.co/to-be-defined.html', +}; describe('Runtime field form', () => { let testBed: TestBed; @@ -30,7 +32,7 @@ describe('Runtime field form', () => { }); test('should render expected 3 fields (name, returnType, script)', () => { - testBed = setup({ docsBaseUri }); + testBed = setup({ links }); const { exists } = testBed; expect(exists('nameField')).toBe(true); @@ -39,11 +41,11 @@ describe('Runtime field form', () => { }); test('should have a link to learn more about painless syntax', () => { - testBed = setup({ docsBaseUri }); + testBed = setup({ links }); const { exists, find } = testBed; expect(exists('painlessSyntaxLearnMoreLink')).toBe(true); - expect(find('painlessSyntaxLearnMoreLink').props().href).toContain(docsBaseUri); + expect(find('painlessSyntaxLearnMoreLink').props().href).toBe(links.painlessSyntax); }); test('should accept a "defaultValue" prop', () => { @@ -52,7 +54,7 @@ describe('Runtime field form', () => { type: 'date', script: 'test=123', }; - testBed = setup({ defaultValue, docsBaseUri }); + testBed = setup({ defaultValue, links }); const { find } = testBed; expect(find('nameField.input').props().value).toBe(defaultValue.name); @@ -66,7 +68,7 @@ describe('Runtime field form', () => { type: 'date', script: 'test=123', }; - testBed = setup({ onChange, defaultValue, docsBaseUri }); + testBed = setup({ onChange, defaultValue, links }); expect(onChange).toHaveBeenCalled(); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx index 77ba4b81d65ee..6068302f5b269 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; +import { PainlessLang } from '@kbn/monaco'; import { EuiFlexGroup, EuiFlexItem, @@ -18,7 +19,6 @@ import { import { useForm, Form, FormHook, UseField, TextField, CodeEditor } from '../../shared_imports'; import { RuntimeField } from '../../types'; import { RUNTIME_FIELD_OPTIONS } from '../../constants'; - import { schema } from './schema'; export interface FormState { @@ -28,12 +28,14 @@ export interface FormState { } export interface Props { - docsBaseUri: string; + links: { + painlessSyntax: string; + }; defaultValue?: RuntimeField; onChange?: (state: FormState) => void; } -const RuntimeFieldFormComp = ({ defaultValue, onChange, docsBaseUri }: Props) => { +const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { const { form } = useForm({ defaultValue, schema }); const { submit, isValid: isFormValid, isSubmitted } = form; @@ -44,7 +46,7 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, docsBaseUri }: Props) => }, [onChange, isFormValid, isSubmitted, submit]); return ( -
+ {/* Name */} @@ -94,7 +96,7 @@ const RuntimeFieldFormComp = ({ defaultValue, onChange, docsBaseUri }: Props) => {/* Script */} path="script"> - {({ value, setValue, label, helpText, isValid, getErrorsMessages }) => { + {({ value, setValue, label, isValid, getErrorsMessages }) => { return ( fullWidth > { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + + return { + painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, + }; +}; diff --git a/x-pack/plugins/runtime_fields/public/lib/index.ts b/x-pack/plugins/runtime_fields/public/lib/index.ts new file mode 100644 index 0000000000000..11c9914bf2e81 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getLinks } from './documentation'; From 2598cb7a76ee50f4dbdf6839815acf11995def89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 4 Nov 2020 14:00:35 +0100 Subject: [PATCH 4/6] =?UTF-8?q?[Runtime=20field=20editor]=C2=A0Expose=20ha?= =?UTF-8?q?ndler=20from=20plugin=20to=20open=20editor=20(#82464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- x-pack/plugins/runtime_fields/README.md | 68 ++++++++++++++- x-pack/plugins/runtime_fields/public/index.ts | 2 +- .../runtime_fields/public/load_editor.tsx | 57 +++++++++++++ .../runtime_fields/public/plugin.test.ts | 82 +++++++++++++++++++ .../plugins/runtime_fields/public/plugin.ts | 5 +- .../runtime_fields/public/shared_imports.ts | 6 +- x-pack/plugins/runtime_fields/public/types.ts | 10 ++- 7 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/runtime_fields/public/load_editor.tsx create mode 100644 x-pack/plugins/runtime_fields/public/plugin.test.ts diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_fields/README.md index f3e2ca5031262..c157b7c44a30d 100644 --- a/x-pack/plugins/runtime_fields/README.md +++ b/x-pack/plugins/runtime_fields/README.md @@ -4,11 +4,71 @@ Welcome to the home of the runtime field editor and everything related to runtim ## The runtime field editor -The runtime field editor is exported in 2 flavours: +### Integration -* As the content of a `` +The recommended way to integrate the runtime fields editor is by adding a plugin dependency to the `"runtimeFields"` x-pack plugin. This way you will be able to lazy load the editor when it is required and it will not increment the bundle size of your plugin. + +```js +// 1. Add the plugin as a dependency in your kibana.json +{ + ... + "requiredBundles": [ + "runtimeFields", + ... + ] +} + +// 2. Access it in your plugin setup() +export class MyPlugin { + setup(core, { runtimeFields }) { + // logic to provide it to your app, probably through context + } +} + +// 3. Load the editor and open it anywhere in your app +const MyComponent = () => { + // Access the plugin through context + const { runtimeFields } = useAppPlugins(); + + // Ref for handler to close the editor + const closeRuntimeFieldEditor = useRef(() => {}); + + const saveRuntimeField = (field: RuntimeField) => { + // Do something with the field + }; + + const openRuntimeFieldsEditor = async() => { + // Lazy load the editor + const { openEditor } = await runtimeFields.loadEditor(); + + closeRuntimeFieldEditor.current = openEditor({ + onSave: saveRuntimeField, + /* defaultValue: optional field to edit */ + }); + }; + + useEffect(() => { + return () => { + // Make sure to remove the editor when the component unmounts + closeRuntimeFieldEditor.current(); + }; + }, []); + + return ( + + ) +} +``` + +#### Alternative + +The runtime field editor is also exported as static React component that you can import into your components. The editor is exported in 2 flavours: + +* As the content of a `` (it contains a flyout header and footer) * As a standalone component that you can inline anywhere +**Note:** The runtime field editor uses the `` that has a dependency on the `Provider` from the `"kibana_react"` plugin. If your app is not already wrapped by this provider you will need to add it at least around the runtime field editor. You can see an example in the ["Using the core.overlays.openFlyout()"](#using-the-coreoverlaysopenflyout) example below. + ### Content of a `` ```js @@ -43,9 +103,9 @@ const MyComponent = () => { } ``` -#### With the `core.overlays.openFlyout` +#### Using the `core.overlays.openFlyout()` -As an alternative you can open the flyout with the `core.overlays.openFlyout`. In this case you will need to wrap the editor with the `Provider` from the "kibana_react" plugin as it is a required dependency for the `` component. +As an alternative you can open the flyout with the `openFlyout()` helper from core. ```js import React, { useRef } from 'react'; diff --git a/x-pack/plugins/runtime_fields/public/index.ts b/x-pack/plugins/runtime_fields/public/index.ts index 98b018089bd37..0eab32c0b3d97 100644 --- a/x-pack/plugins/runtime_fields/public/index.ts +++ b/x-pack/plugins/runtime_fields/public/index.ts @@ -11,7 +11,7 @@ export { RuntimeFieldFormState, } from './components'; export { RUNTIME_FIELD_OPTIONS } from './constants'; -export { RuntimeField, RuntimeType } from './types'; +export { RuntimeField, RuntimeType, PluginSetup as RuntimeFieldsSetup } from './types'; export function plugin() { return new RuntimeFieldsPlugin(); diff --git a/x-pack/plugins/runtime_fields/public/load_editor.tsx b/x-pack/plugins/runtime_fields/public/load_editor.tsx new file mode 100644 index 0000000000000..f1b9c495f0336 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/load_editor.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { CoreSetup, OverlayRef } from 'src/core/public'; + +import { toMountPoint, createKibanaReactContext } from './shared_imports'; +import { LoadEditorResponse, RuntimeField } from './types'; + +export interface OpenRuntimeFieldEditorProps { + onSave(field: RuntimeField): void; + defaultValue?: RuntimeField; +} + +export const getRuntimeFieldEditorLoader = (coreSetup: CoreSetup) => async (): Promise< + LoadEditorResponse +> => { + const { RuntimeFieldEditorFlyoutContent } = await import('./components'); + const [core] = await coreSetup.getStartServices(); + const { uiSettings, overlays, docLinks } = core; + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings }); + + let overlayRef: OverlayRef | null = null; + + const openEditor = ({ onSave, defaultValue }: OpenRuntimeFieldEditorProps) => { + const closeEditor = () => { + overlayRef?.close(); + overlayRef = null; + }; + + const onSaveField = (field: RuntimeField) => { + closeEditor(); + onSave(field); + }; + + overlayRef = overlays.openFlyout( + toMountPoint( + + overlayRef?.close()} + docLinks={docLinks} + defaultValue={defaultValue} + /> + + ) + ); + + return closeEditor; + }; + + return { + openEditor, + }; +}; diff --git a/x-pack/plugins/runtime_fields/public/plugin.test.ts b/x-pack/plugins/runtime_fields/public/plugin.test.ts new file mode 100644 index 0000000000000..07f7a3553d0d3 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/plugin.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; + +jest.mock('../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../src/plugins/kibana_react/public'); + + return { + ...original, + toMountPoint: (node: React.ReactNode) => node, + }; +}); + +import { StartPlugins, PluginStart } from './types'; +import { RuntimeFieldEditorFlyoutContent } from './components'; +import { RuntimeFieldsPlugin } from './plugin'; + +const noop = () => {}; + +describe('RuntimeFieldsPlugin', () => { + let coreSetup: CoreSetup; + let plugin: RuntimeFieldsPlugin; + + beforeEach(() => { + plugin = new RuntimeFieldsPlugin(); + coreSetup = coreMock.createSetup(); + }); + + test('should return a handler to load the runtime field editor', async () => { + const setupApi = await plugin.setup(coreSetup, {}); + expect(setupApi.loadEditor).toBeDefined(); + }); + + test('once it is loaded it should expose a handler to open the editor', async () => { + const setupApi = await plugin.setup(coreSetup, {}); + const response = await setupApi.loadEditor(); + expect(response.openEditor).toBeDefined(); + }); + + test('should call core.overlays.openFlyout when opening the editor', async () => { + const openFlyout = jest.fn(); + const onSaveSpy = jest.fn(); + + const mockCore = { + overlays: { + openFlyout, + }, + uiSettings: {}, + }; + coreSetup.getStartServices = async () => [mockCore] as any; + const setupApi = await plugin.setup(coreSetup, {}); + const { openEditor } = await setupApi.loadEditor(); + + openEditor({ onSave: onSaveSpy }); + + expect(openFlyout).toHaveBeenCalled(); + + const [[arg]] = openFlyout.mock.calls; + expect(arg.props.children.type).toBe(RuntimeFieldEditorFlyoutContent); + + // We force call the "onSave" prop from the component + // and make sure that the the spy is being called. + // Note: we are testing implementation details, if we change or rename the "onSave" prop on + // the component, we will need to update this test accordingly. + expect(arg.props.children.props.onSave).toBeDefined(); + arg.props.children.props.onSave(); + expect(onSaveSpy).toHaveBeenCalled(); + }); + + test('should return a handler to close the flyout', async () => { + const setupApi = await plugin.setup(coreSetup, {}); + const { openEditor } = await setupApi.loadEditor(); + + const closeEditorHandler = openEditor({ onSave: noop }); + expect(typeof closeEditorHandler).toBe('function'); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/plugin.ts b/x-pack/plugins/runtime_fields/public/plugin.ts index d893a1e181811..ebc8b98db66ba 100644 --- a/x-pack/plugins/runtime_fields/public/plugin.ts +++ b/x-pack/plugins/runtime_fields/public/plugin.ts @@ -6,11 +6,14 @@ import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; +import { getRuntimeFieldEditorLoader } from './load_editor'; export class RuntimeFieldsPlugin implements Plugin { public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { - return {}; + return { + loadEditor: getRuntimeFieldEditorLoader(core), + }; } public start(core: CoreStart, plugins: StartPlugins) { diff --git a/x-pack/plugins/runtime_fields/public/shared_imports.ts b/x-pack/plugins/runtime_fields/public/shared_imports.ts index 8ce22a66b627b..200a68ab71031 100644 --- a/x-pack/plugins/runtime_fields/public/shared_imports.ts +++ b/x-pack/plugins/runtime_fields/public/shared_imports.ts @@ -16,4 +16,8 @@ export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/for export { TextField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; -export { CodeEditor } from '../../../../src/plugins/kibana_react/public'; +export { + CodeEditor, + toMountPoint, + createKibanaReactContext, +} from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/runtime_fields/public/types.ts b/x-pack/plugins/runtime_fields/public/types.ts index 9d1daa9eacb0e..4172061540af8 100644 --- a/x-pack/plugins/runtime_fields/public/types.ts +++ b/x-pack/plugins/runtime_fields/public/types.ts @@ -6,9 +6,15 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { RUNTIME_FIELD_TYPES } from './constants'; +import { OpenRuntimeFieldEditorProps } from './load_editor'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginSetup {} +export interface LoadEditorResponse { + openEditor(props: OpenRuntimeFieldEditorProps): () => void; +} + +export interface PluginSetup { + loadEditor(): Promise; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} From 1693eb4bb7ca9eac785360e0da8138baf8dfe781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 10 Nov 2020 09:58:29 +0100 Subject: [PATCH 5/6] Update README.md with data returned by the editor --- x-pack/plugins/runtime_fields/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_fields/README.md index c157b7c44a30d..d4664a3a07c61 100644 --- a/x-pack/plugins/runtime_fields/README.md +++ b/x-pack/plugins/runtime_fields/README.md @@ -30,11 +30,12 @@ const MyComponent = () => { // Access the plugin through context const { runtimeFields } = useAppPlugins(); - // Ref for handler to close the editor + // Ref of the handler to close the editor const closeRuntimeFieldEditor = useRef(() => {}); const saveRuntimeField = (field: RuntimeField) => { // Do something with the field + console.log(field); // { name: 'myField', type: 'boolean', script: "return 'hello'" } }; const openRuntimeFieldsEditor = async() => { From 9fffc907087250f08affa7e96e4d5c850e86eea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Loix?= Date: Tue, 17 Nov 2020 10:17:36 +0100 Subject: [PATCH 6/6] Fix path to Testbed package --- x-pack/plugins/runtime_fields/public/test_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/runtime_fields/public/test_utils.ts b/x-pack/plugins/runtime_fields/public/test_utils.ts index fb6c6cf2a79a5..966db01ef1532 100644 --- a/x-pack/plugins/runtime_fields/public/test_utils.ts +++ b/x-pack/plugins/runtime_fields/public/test_utils.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerTestBed, TestBed } from '../../../test_utils/testbed'; +export { registerTestBed, TestBed } from '@kbn/test/jest';