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 {}