diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e89b6d86361c7..1436399a03dbc 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -479,6 +479,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/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/saved_objects_tagging/README.md[savedObjectsTagging] |Add tagging capability to saved objects diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index e326c8e2cac39..7cdbe844c2901 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,4 +102,5 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 + runtimeFields: 41752 stackAlerts: 29684 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 2be68b797ba5f..6937862d20536 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -38,10 +38,11 @@ "xpack.maps": ["plugins/maps"], "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], - "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", + "xpack.remoteClusters": "plugins/remote_clusters", "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 ff6a71d53894a..4e4ad9b8e1d31 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -18,6 +18,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/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 = ( { 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/README.md b/x-pack/plugins/runtime_fields/README.md new file mode 100644 index 0000000000000..d4664a3a07c61 --- /dev/null +++ b/x-pack/plugins/runtime_fields/README.md @@ -0,0 +1,197 @@ +# Runtime fields + +Welcome to the home of the runtime field editor and everything related to runtime fields! + +## The runtime field editor + +### Integration + +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 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() => { + // 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 +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*/} + /> + + )} + + ) +} +``` + +#### Using the `core.overlays.openFlyout()` + +As an alternative you can open the flyout with the `openFlyout()` helper from core. + +```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/kibana.json b/x-pack/plugins/runtime_fields/kibana.json new file mode 100644 index 0000000000000..65932c723c474 --- /dev/null +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "runtimeFields", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [ + ], + "optionalPlugins": [ + ], + "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..ccfe426cfdb09 --- /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(e.target.value); + }} + /> + ); + + 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..86ac968d39f21 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { 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 new file mode 100644 index 0000000000000..4041a04aec4d1 --- /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, 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 new file mode 100644 index 0000000000000..1829514856eed --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 links = { + painlessSyntax: 'https://jestTest.elastic.co/to-be-defined.html', +}; + +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({ links }); + 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({ links }); + const { exists, find } = testBed; + + expect(exists('painlessSyntaxLearnMoreLink')).toBe(true); + expect(find('painlessSyntaxLearnMoreLink').props().href).toBe(links.painlessSyntax); + }); + + test('should accept a "defaultValue" prop', () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ defaultValue, links }); + 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, links }); + + 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..6068302f5b269 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -0,0 +1,149 @@ +/* + * 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 { PainlessLang } from '@kbn/monaco'; +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 { + links: { + painlessSyntax: string; + }; + defaultValue?: RuntimeField; + onChange?: (state: FormState) => void; +} + +const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: 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, 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..0eab32c0b3d97 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { + RuntimeFieldEditorFlyoutContent, + RuntimeFieldEditor, + RuntimeFieldFormState, +} from './components'; +export { RUNTIME_FIELD_OPTIONS } from './constants'; +export { RuntimeField, RuntimeType, PluginSetup as RuntimeFieldsSetup } from './types'; + +export function plugin() { + return new RuntimeFieldsPlugin(); +} diff --git a/x-pack/plugins/runtime_fields/public/lib/documentation.ts b/x-pack/plugins/runtime_fields/public/lib/documentation.ts new file mode 100644 index 0000000000000..87eab8b7ed997 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/lib/documentation.ts @@ -0,0 +1,16 @@ +/* + * 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 { DocLinksStart } from 'src/core/public'; + +export const getLinks = (docLinks: DocLinksStart) => { + 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'; 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 new file mode 100644 index 0000000000000..ebc8b98db66ba --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/plugin.ts @@ -0,0 +1,26 @@ +/* + * 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'; +import { getRuntimeFieldEditorLoader } from './load_editor'; + +export class RuntimeFieldsPlugin + implements Plugin { + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { + return { + loadEditor: getRuntimeFieldEditorLoader(core), + }; + } + + 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..200a68ab71031 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/shared_imports.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. + */ + +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, + toMountPoint, + createKibanaReactContext, +} 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..966db01ef1532 --- /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 '@kbn/test/jest'; 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..4172061540af8 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/types.ts @@ -0,0 +1,40 @@ +/* + * 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'; +import { OpenRuntimeFieldEditorProps } from './load_editor'; + +export interface LoadEditorResponse { + openEditor(props: OpenRuntimeFieldEditorProps): () => void; +} + +export interface PluginSetup { + loadEditor(): Promise; +} + +// 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; +}