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