diff --git a/docs/development/core/public/kibana-plugin-public.contextcontainer.md b/docs/development/core/public/kibana-plugin-public.contextcontainer.md index 8b1ae8c9e96ada5..621683bbb608f94 100644 --- a/docs/development/core/public/kibana-plugin-public.contextcontainer.md +++ b/docs/development/core/public/kibana-plugin-public.contextcontainer.md @@ -21,7 +21,68 @@ export interface ContextContainer this.handlers.set( + path, + this.contextContainer.createHandler(handler) + )); + } + } + } +} + +// ALSO GOOD +class MyPlugin { + private readonly handlers = new Map(); + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + // Returning a Promise also works, but only if calling plugins wait for it to resolve before returning from + // their lifecycle hooks. + async registerRoute(path, handler) { + await doAsyncThing(); + this.handlers.set( + path, + this.contextContainer.createHandler(handler) + ); + } + } + } +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.md b/docs/development/core/public/kibana-plugin-public.contextsetup.md index f1c8db5c8fd231f..bb8309634d6d272 100644 --- a/docs/development/core/public/kibana-plugin-public.contextsetup.md +++ b/docs/development/core/public/kibana-plugin-public.contextsetup.md @@ -20,30 +20,122 @@ export interface ContextSetup ## Remarks -A `ContextContainer` can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and building a context object for a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares. +A [ContextContainer](./kibana-plugin-public.contextcontainer.md) can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and building a context object for a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares. Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on. +In order to configure a handler with context, you must call the [ContextContainer.createHandler()](./kibana-plugin-public.contextcontainer.createhandler.md) function. This function must be called \_while the calling plugin's lifecycle method is still running\_ or else you risk configuring the handler for the wrong plugin, or not plugin at all (the latter will throw an error). + +```ts +// GOOD +class MyPlugin { + private readonly handlers = new Map(); + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + registerRoute(path, handler) { + this.handlers.set( + path, + this.contextContainer.createHandler(handler) + ); + } + } + } +} + +// BAD +class MyPlugin { + private readonly handlers = new Map(); + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + // When the promise isn't returned, it's possible `createHandler` won't be called until after the lifecycle + // hook is completed. + registerRoute(path, handler) { + doAsyncThing().then(() => this.handlers.set( + path, + this.contextContainer.createHandler(handler) + )); + } + } + } +} + +// ALSO GOOD +class MyPlugin { + private readonly handlers = new Map(); + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + // Returning a Promise also works, but only if calling plugins wait for it to resolve before returning from + // their lifecycle hooks. + async registerRoute(path, handler) { + await doAsyncThing(); + this.handlers.set( + path, + this.contextContainer.createHandler(handler) + ); + } + } + } +} + +``` + ## Example -How to create your own context +Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. ```ts -class MyPlugin { +export interface VizRenderContext { + core: { + i18n: I18nStart; + uiSettings: UISettingsClientContract; + } + [contextName: string]: unknown; +} + +export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + +class VizRenderingPlugin { + private readonly vizRenderers = new Map () => void)>(); + setup(core) { - this.myHandlers = new Map(); - this.contextContainer = core.createContextContainer(); + this.contextContainer = core.createContextContainer< + VizRenderContext, + ReturnType, + [HTMLElement] + >(); + return { registerContext: this.contextContainer.register, - registerHandler: (endpoint, handler) => - // `createHandler` must be called immediately. - this.myHandlers.set(endpoint, this.contextContainer.createHandler(handler)), + registerVizRenderer: (renderMethod: string, renderer: VizTypeRenderer) => + // `createHandler` must be called immediately during the calling plugin's lifecycle method. + this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(renderer)), }; } - start() { + start(core) { + // Register the core context available to all renderers + this.contextContainer.register('core', () => ({ + i18n: core.i18n, + uiSettings: core.uiSettings + })); + return { registerContext: this.contextContainer.register, + // The handler can now be called directly with only an `HTMLElement` and will automaticallly + // have the `context` argument supplied. + renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + if (!this.vizRenderer.has(renderMethod)) { + throw new Error(`Render method ${renderMethod} has not be registered`); + } + + return this.vizRenderers.get(renderMethod)(domElement); + } }; } } diff --git a/src/core/public/context/context.ts b/src/core/public/context/context.ts index 669f70ffeacb95d..73f8de89f429cc8 100644 --- a/src/core/public/context/context.ts +++ b/src/core/public/context/context.ts @@ -61,7 +61,7 @@ type Promisify = T extends Promise ? Promise : Promise; * An object that handles registration of context providers and building of new context objects. * * @remarks - * A `ContextContainer` can be used by any Core service or plugin (known as the "service owner") which wishes to expose + * A {@link ContextContainer} can be used by any Core service or plugin (known as the "service owner") which wishes to expose * APIs in a handler function. The container object will manage registering context providers and building a context * object for a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on * the dependencies that the handler's plugin declares. @@ -69,6 +69,68 @@ type Promisify = T extends Promise ? Promise : Promise; * Contexts providers are executed in the order they were registered. Each provider gets access to context values * provided by any plugins that it depends on. * + * In order to configure a handler with context, you must call the {@link ContextContainer.createHandler} function. This + * function must be called _while the calling plugin's lifecycle method is still running_ or else you risk configuring + * the handler for the wrong plugin, or not plugin at all (the latter will throw an error). + * + * ```ts + * // GOOD + * class MyPlugin { + * private readonly handlers = new Map(); + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer(); + * return { + * registerRoute(path, handler) { + * this.handlers.set( + * path, + * this.contextContainer.createHandler(handler) + * ); + * } + * } + * } + * } + * + * // BAD + * class MyPlugin { + * private readonly handlers = new Map(); + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer(); + * return { + * // When the promise isn't returned, it's possible `createHandler` won't be called until after the lifecycle + * // hook is completed. + * registerRoute(path, handler) { + * doAsyncThing().then(() => this.handlers.set( + * path, + * this.contextContainer.createHandler(handler) + * )); + * } + * } + * } + * } + * + * // ALSO GOOD + * class MyPlugin { + * private readonly handlers = new Map(); + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer(); + * return { + * // Returning a Promise also works, but only if calling plugins wait for it to resolve before returning from + * // their lifecycle hooks. + * async registerRoute(path, handler) { + * await doAsyncThing(); + * this.handlers.set( + * path, + * this.contextContainer.createHandler(handler) + * ); + * } + * } + * } + * } + * ``` + * * * @public */ diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts index c88198bb7b27110..ac75a1593d90244 100644 --- a/src/core/public/context/context_service.ts +++ b/src/core/public/context/context_service.ts @@ -62,23 +62,55 @@ export class ContextService { * {@inheritdoc ContextContainer} * * @example - * How to create your own context + * Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we + * want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. * ```ts - * class MyPlugin { + * export interface VizRenderContext { + * core: { + * i18n: I18nStart; + * uiSettings: UISettingsClientContract; + * } + * [contextName: string]: unknown; + * } + * + * export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + * + * class VizRenderingPlugin { + * private readonly vizRenderers = new Map () => void)>(); + * * setup(core) { - * this.myHandlers = new Map(); - * this.contextContainer = core.createContextContainer(); + * this.contextContainer = core.createContextContainer< + * VizRenderContext, + * ReturnType, + * [HTMLElement] + * >(); + * * return { * registerContext: this.contextContainer.register, - * registerHandler: (endpoint, handler) => - * // `createHandler` must be called immediately. - * this.myHandlers.set(endpoint, this.contextContainer.createHandler(handler)), + * registerVizRenderer: (renderMethod: string, renderer: VizTypeRenderer) => + * // `createHandler` must be called immediately during the calling plugin's lifecycle method. + * this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(renderer)), * }; * } * - * start() { + * start(core) { + * // Register the core context available to all renderers + * this.contextContainer.register('core', () => ({ + * i18n: core.i18n, + * uiSettings: core.uiSettings + * })); + * * return { * registerContext: this.contextContainer.register, + * // The handler can now be called directly with only an `HTMLElement` and will automaticallly + * // have the `context` argument supplied. + * renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + * if (!this.vizRenderer.has(renderMethod)) { + * throw new Error(`Render method ${renderMethod} has not be registered`); + * } + * + * return this.vizRenderers.get(renderMethod)(domElement); + * } * }; * } * }