mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
parent
9be59c0b10
commit
1898510de8
35 changed files with 1353 additions and 79 deletions
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextSetup](./kibana-plugin-public.contextsetup.md) > [createContextContainer](./kibana-plugin-public.contextsetup.createcontextcontainer.md)
|
||||
|
||||
## ContextSetup.createContextContainer() method
|
||||
|
||||
Creates a new [IContextContainer](./kibana-plugin-public.icontextcontainer.md) for a service owner.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
createContextContainer<TContext extends {}, THandlerReturn, THandlerParmaters extends any[] = []>(): IContextContainer<TContext, THandlerReturn, THandlerParmaters>;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`IContextContainer<TContext, THandlerReturn, THandlerParmaters>`
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextSetup](./kibana-plugin-public.contextsetup.md)
|
||||
|
||||
## ContextSetup interface
|
||||
|
||||
An object that handles registration of context providers and configuring handlers with context.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ContextSetup
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [createContextContainer()](./kibana-plugin-public.contextsetup.createcontextcontainer.md) | Creates a new [IContextContainer](./kibana-plugin-public.icontextcontainer.md) for a service owner. |
|
||||
|
||||
## Remarks
|
||||
|
||||
A [IContextContainer](./kibana-plugin-public.icontextcontainer.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 configuring 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 [IContextContainer.createHandler()](./kibana-plugin-public.icontextcontainer.createhandler.md) function and use the returned handler which will automatically build a context object when called.
|
||||
|
||||
When registering context or creating handlers, the \_calling plugin's opaque id\_ must be provided. This id is passed in via the plugin's initializer and can be accessed from the [PluginInitializerContext.opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) Note this should NOT be the context service owner's id, but the plugin that is actually registering the context or handler.
|
||||
|
||||
```ts
|
||||
// Correct
|
||||
class MyPlugin {
|
||||
private readonly handlers = new Map();
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.context.createContextContainer();
|
||||
return {
|
||||
registerContext(pluginOpaqueId, contextName, provider) {
|
||||
this.contextContainer.registerContext(pluginOpaqueId, contextName, provider);
|
||||
},
|
||||
registerRoute(pluginOpaqueId, path, handler) {
|
||||
this.handlers.set(
|
||||
path,
|
||||
this.contextContainer.createHandler(pluginOpaqueId, handler)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Incorrect
|
||||
class MyPlugin {
|
||||
private readonly handlers = new Map();
|
||||
|
||||
constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.context.createContextContainer();
|
||||
return {
|
||||
registerContext(contextName, provider) {
|
||||
// BUG!
|
||||
// This would leak this context to all handlers rather that only plugins that depend on the calling plugin.
|
||||
this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider);
|
||||
},
|
||||
registerRoute(path, handler) {
|
||||
this.handlers.set(
|
||||
path,
|
||||
// BUG!
|
||||
// This handler will not receive any contexts provided by other dependencies of the calling plugin.
|
||||
this.contextContainer.createHandler(this.initContext.opaqueId, handler)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
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
|
||||
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<string, ((domElement: HTMLElement) => () => void)>();
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.createContextContainer<
|
||||
VizRenderContext,
|
||||
ReturnType<VizRenderer>,
|
||||
[HTMLElement]
|
||||
>();
|
||||
|
||||
return {
|
||||
registerContext: this.contextContainer.registerContext,
|
||||
registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) =>
|
||||
this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)),
|
||||
};
|
||||
}
|
||||
|
||||
start(core) {
|
||||
// Register the core context available to all renderers. Use the VizRendererContext's pluginId as the first arg.
|
||||
this.contextContainer.registerContext('viz_rendering', 'core', () => ({
|
||||
i18n: core.i18n,
|
||||
uiSettings: core.uiSettings
|
||||
}));
|
||||
|
||||
return {
|
||||
registerContext: this.contextContainer.registerContext,
|
||||
|
||||
renderVizualization: (renderMethod: string, domElement: HTMLElement) => {
|
||||
if (!this.vizRenderer.has(renderMethod)) {
|
||||
throw new Error(`Render method '${renderMethod}' has not been registered`);
|
||||
}
|
||||
|
||||
// The handler can now be called directly with only an `HTMLElement` and will automatically
|
||||
// have a new `context` object created and populated by the context container.
|
||||
const handler = this.vizRenderers.get(renderMethod)
|
||||
return handler(domElement);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [context](./kibana-plugin-public.coresetup.context.md)
|
||||
|
||||
## CoreSetup.context property
|
||||
|
||||
[ContextSetup](./kibana-plugin-public.contextsetup.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
context: ContextSetup;
|
||||
```
|
|
@ -16,6 +16,7 @@ export interface CoreSetup
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [context](./kibana-plugin-public.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-public.contextsetup.md) |
|
||||
| [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | <code>FatalErrorsSetup</code> | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) |
|
||||
| [http](./kibana-plugin-public.coresetup.http.md) | <code>HttpSetup</code> | [HttpSetup](./kibana-plugin-public.httpsetup.md) |
|
||||
| [notifications](./kibana-plugin-public.coresetup.notifications.md) | <code>NotificationsSetup</code> | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) |
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextContainer](./kibana-plugin-public.icontextcontainer.md) > [createHandler](./kibana-plugin-public.icontextcontainer.createhandler.md)
|
||||
|
||||
## IContextContainer.createHandler() method
|
||||
|
||||
Create a new handler function pre-wired to context for the plugin.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler<TContext, THandlerReturn, THandlerParameters>): (...rest: THandlerParameters) => Promisify<THandlerReturn>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| pluginOpaqueId | <code>PluginOpaqueId</code> | The plugin opaque ID for the plugin that registers this handler. |
|
||||
| handler | <code>IContextHandler<TContext, THandlerReturn, THandlerParameters></code> | Handler function to pass context object to. |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`(...rest: THandlerParameters) => Promisify<THandlerReturn>`
|
||||
|
||||
A function that takes `THandlerParameters`<!-- -->, calls `handler` with a new context, and returns a Promise of the `handler` return value.
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextContainer](./kibana-plugin-public.icontextcontainer.md)
|
||||
|
||||
## IContextContainer interface
|
||||
|
||||
An object that handles registration of context providers and configuring handlers with context.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface IContextContainer<TContext extends {}, THandlerReturn, THandlerParameters extends any[] = []>
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [createHandler(pluginOpaqueId, handler)](./kibana-plugin-public.icontextcontainer.createhandler.md) | Create a new handler function pre-wired to context for the plugin. |
|
||||
| [registerContext(pluginOpaqueId, contextName, provider)](./kibana-plugin-public.icontextcontainer.registercontext.md) | Register a new context provider. |
|
||||
|
||||
## Remarks
|
||||
|
||||
A [IContextContainer](./kibana-plugin-public.icontextcontainer.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 configuring 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 [IContextContainer.createHandler()](./kibana-plugin-public.icontextcontainer.createhandler.md) function and use the returned handler which will automatically build a context object when called.
|
||||
|
||||
When registering context or creating handlers, the \_calling plugin's opaque id\_ must be provided. This id is passed in via the plugin's initializer and can be accessed from the [PluginInitializerContext.opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) Note this should NOT be the context service owner's id, but the plugin that is actually registering the context or handler.
|
||||
|
||||
```ts
|
||||
// Correct
|
||||
class MyPlugin {
|
||||
private readonly handlers = new Map();
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.context.createContextContainer();
|
||||
return {
|
||||
registerContext(pluginOpaqueId, contextName, provider) {
|
||||
this.contextContainer.registerContext(pluginOpaqueId, contextName, provider);
|
||||
},
|
||||
registerRoute(pluginOpaqueId, path, handler) {
|
||||
this.handlers.set(
|
||||
path,
|
||||
this.contextContainer.createHandler(pluginOpaqueId, handler)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Incorrect
|
||||
class MyPlugin {
|
||||
private readonly handlers = new Map();
|
||||
|
||||
constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
|
||||
setup(core) {
|
||||
this.contextContainer = core.context.createContextContainer();
|
||||
return {
|
||||
registerContext(contextName, provider) {
|
||||
// BUG!
|
||||
// This would leak this context to all handlers rather that only plugins that depend on the calling plugin.
|
||||
this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider);
|
||||
},
|
||||
registerRoute(path, handler) {
|
||||
this.handlers.set(
|
||||
path,
|
||||
// BUG!
|
||||
// This handler will not receive any contexts provided by other dependencies of the calling plugin.
|
||||
this.contextContainer.createHandler(this.initContext.opaqueId, handler)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextContainer](./kibana-plugin-public.icontextcontainer.md) > [registerContext](./kibana-plugin-public.icontextcontainer.registercontext.md)
|
||||
|
||||
## IContextContainer.registerContext() method
|
||||
|
||||
Register a new context provider.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
registerContext<TContextName extends keyof TContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider<TContext, TContextName, THandlerParameters>): this;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| pluginOpaqueId | <code>PluginOpaqueId</code> | The plugin opaque ID for the plugin that registers this context. |
|
||||
| contextName | <code>TContextName</code> | The key of the <code>TContext</code> object this provider supplies the value for. |
|
||||
| provider | <code>IContextProvider<TContext, TContextName, THandlerParameters></code> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) to be called each time a new context is created. |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`this`
|
||||
|
||||
The [IContextContainer](./kibana-plugin-public.icontextcontainer.md) for method chaining.
|
||||
|
||||
## Remarks
|
||||
|
||||
The value (or resolved Promise value) returned by the `provider` function will be attached to the context object on the key specified by `contextName`<!-- -->.
|
||||
|
||||
Throws an exception if more than one provider is registered for the same `contextName`<!-- -->.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextHandler](./kibana-plugin-public.icontexthandler.md)
|
||||
|
||||
## IContextHandler type
|
||||
|
||||
A function registered by a plugin to perform some action.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type IContextHandler<TContext extends {}, TReturn, THandlerParameters extends any[] = []> = (context: TContext, ...rest: THandlerParameters) => TReturn;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
A new `TContext` will be built for each handler before invoking.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextProvider](./kibana-plugin-public.icontextprovider.md)
|
||||
|
||||
## IContextProvider type
|
||||
|
||||
A function that returns a context value for a specific key of given context type.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type IContextProvider<TContext extends Record<string, any>, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial<TContext>, ...rest: TProviderParameters) => Promise<TContext[TContextName]> | TContext[TContextName];
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
||||
This function will be called each time a new context is built for a handler invocation.
|
||||
|
|
@ -34,6 +34,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
|
||||
| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
|
||||
| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
|
||||
| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. |
|
||||
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the <code>Plugin</code> setup lifecycle |
|
||||
| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the <code>Plugin</code> start lifecycle |
|
||||
| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |
|
||||
|
@ -50,6 +51,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [HttpResponse](./kibana-plugin-public.httpresponse.md) | |
|
||||
| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | |
|
||||
| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
|
||||
| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
|
||||
| [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | |
|
||||
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
|
||||
| [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | |
|
||||
|
@ -69,6 +71,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [HttpHandler](./kibana-plugin-public.httphandler.md) | |
|
||||
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
|
||||
| [HttpStart](./kibana-plugin-public.httpstart.md) | |
|
||||
| [IContextHandler](./kibana-plugin-public.icontexthandler.md) | A function registered by a plugin to perform some action. |
|
||||
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
|
||||
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
|
||||
| [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | |
|
||||
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
|
||||
|
|
|
@ -11,3 +11,10 @@ The available core services passed to a `PluginInitializer`
|
|||
```typescript
|
||||
export interface PluginInitializerContext
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | <code>PluginOpaqueId</code> | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) > [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md)
|
||||
|
||||
## PluginInitializerContext.opaqueId property
|
||||
|
||||
A symbol used to identify this plugin in the system. Needed when registering handlers or context providers.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly opaqueId: PluginOpaqueId;
|
||||
```
|
34
src/core/public/context/context.mock.ts
Normal file
34
src/core/public/context/context.mock.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { IContextContainer } from './context';
|
||||
|
||||
export type ContextContainerMock = jest.Mocked<IContextContainer<any, any, any>>;
|
||||
|
||||
const createContextMock = () => {
|
||||
const contextMock: ContextContainerMock = {
|
||||
registerContext: jest.fn(),
|
||||
createHandler: jest.fn(),
|
||||
};
|
||||
return contextMock;
|
||||
};
|
||||
|
||||
export const contextMock = {
|
||||
create: createContextMock,
|
||||
};
|
232
src/core/public/context/context.test.ts
Normal file
232
src/core/public/context/context.test.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ContextContainer } from './context';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
|
||||
const pluginA = Symbol('pluginA');
|
||||
const pluginB = Symbol('pluginB');
|
||||
const pluginC = Symbol('pluginC');
|
||||
const pluginD = Symbol('pluginD');
|
||||
const plugins: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]> = new Map([
|
||||
[pluginA, []],
|
||||
[pluginB, [pluginA]],
|
||||
[pluginC, [pluginA, pluginB]],
|
||||
[pluginD, []],
|
||||
]);
|
||||
|
||||
interface MyContext {
|
||||
core1: string;
|
||||
core2: number;
|
||||
ctxFromA: string;
|
||||
ctxFromB: number;
|
||||
ctxFromC: boolean;
|
||||
ctxFromD: object;
|
||||
}
|
||||
|
||||
const coreId = Symbol();
|
||||
|
||||
describe('ContextContainer', () => {
|
||||
it('does not allow the same context to be registered twice', () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
contextContainer.registerContext(coreId, 'ctxFromA', () => 'aString');
|
||||
|
||||
expect(() =>
|
||||
contextContainer.registerContext(coreId, 'ctxFromA', () => 'aString')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Context provider for ctxFromA has already been registered."`
|
||||
);
|
||||
});
|
||||
|
||||
describe('registerContext', () => {
|
||||
it('throws error if called with an unknown symbol', async () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
await expect(() =>
|
||||
contextContainer.registerContext(Symbol('unknown'), 'ctxFromA', jest.fn())
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot register context for unknown plugin: Symbol(unknown)"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context building', () => {
|
||||
it('resolves dependencies', async () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
expect.assertions(8);
|
||||
contextContainer.registerContext(coreId, 'core1', context => {
|
||||
expect(context).toEqual({});
|
||||
return 'core';
|
||||
});
|
||||
|
||||
contextContainer.registerContext(pluginA, 'ctxFromA', context => {
|
||||
expect(context).toEqual({ core1: 'core' });
|
||||
return 'aString';
|
||||
});
|
||||
contextContainer.registerContext(pluginB, 'ctxFromB', context => {
|
||||
expect(context).toEqual({ core1: 'core', ctxFromA: 'aString' });
|
||||
return 299;
|
||||
});
|
||||
contextContainer.registerContext(pluginC, 'ctxFromC', context => {
|
||||
expect(context).toEqual({ core1: 'core', ctxFromA: 'aString', ctxFromB: 299 });
|
||||
return false;
|
||||
});
|
||||
contextContainer.registerContext(pluginD, 'ctxFromD', context => {
|
||||
expect(context).toEqual({ core1: 'core' });
|
||||
return {};
|
||||
});
|
||||
|
||||
const rawHandler1 = jest.fn<string, []>(() => 'handler1');
|
||||
const handler1 = contextContainer.createHandler(pluginC, rawHandler1);
|
||||
|
||||
const rawHandler2 = jest.fn<string, []>(() => 'handler2');
|
||||
const handler2 = contextContainer.createHandler(pluginD, rawHandler2);
|
||||
|
||||
await handler1();
|
||||
await handler2();
|
||||
|
||||
// Should have context from pluginC, its deps, and core
|
||||
expect(rawHandler1).toHaveBeenCalledWith({
|
||||
core1: 'core',
|
||||
ctxFromA: 'aString',
|
||||
ctxFromB: 299,
|
||||
ctxFromC: false,
|
||||
});
|
||||
|
||||
// Should have context from pluginD, and core
|
||||
expect(rawHandler2).toHaveBeenCalledWith({
|
||||
core1: 'core',
|
||||
ctxFromD: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes all core context to core providers', async () => {
|
||||
expect.assertions(4);
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
|
||||
contextContainer
|
||||
.registerContext(coreId, 'core1', context => {
|
||||
expect(context).toEqual({});
|
||||
return 'core';
|
||||
})
|
||||
.registerContext(coreId, 'core2', context => {
|
||||
expect(context).toEqual({ core1: 'core' });
|
||||
return 101;
|
||||
});
|
||||
|
||||
const rawHandler1 = jest.fn<string, []>(() => 'handler1');
|
||||
const handler1 = contextContainer.createHandler(pluginA, rawHandler1);
|
||||
|
||||
expect(await handler1()).toEqual('handler1');
|
||||
|
||||
// If no context is registered for pluginA, only core contexts should be exposed
|
||||
expect(rawHandler1).toHaveBeenCalledWith({
|
||||
core1: 'core',
|
||||
core2: 101,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not expose plugin contexts to core handler', async () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
|
||||
contextContainer
|
||||
.registerContext(coreId, 'core1', context => 'core')
|
||||
.registerContext(pluginA, 'ctxFromA', context => 'aString');
|
||||
|
||||
const rawHandler1 = jest.fn<string, []>(() => 'handler1');
|
||||
const handler1 = contextContainer.createHandler(coreId, rawHandler1);
|
||||
|
||||
expect(await handler1()).toEqual('handler1');
|
||||
// pluginA context should not be present in a core handler
|
||||
expect(rawHandler1).toHaveBeenCalledWith({
|
||||
core1: 'core',
|
||||
});
|
||||
});
|
||||
|
||||
it('passes additional arguments to providers', async () => {
|
||||
expect.assertions(6);
|
||||
const contextContainer = new ContextContainer<MyContext, string, [string, number]>(
|
||||
plugins,
|
||||
coreId
|
||||
);
|
||||
|
||||
contextContainer.registerContext(coreId, 'core1', (context, str, num) => {
|
||||
expect(str).toEqual('passed string');
|
||||
expect(num).toEqual(77);
|
||||
return `core ${str}`;
|
||||
});
|
||||
|
||||
contextContainer.registerContext(pluginD, 'ctxFromD', (context, str, num) => {
|
||||
expect(str).toEqual('passed string');
|
||||
expect(num).toEqual(77);
|
||||
return {
|
||||
num: 77,
|
||||
};
|
||||
});
|
||||
|
||||
const rawHandler1 = jest.fn<string, [MyContext, string, number]>(() => 'handler1');
|
||||
const handler1 = contextContainer.createHandler(pluginD, rawHandler1);
|
||||
|
||||
expect(await handler1('passed string', 77)).toEqual('handler1');
|
||||
|
||||
expect(rawHandler1).toHaveBeenCalledWith(
|
||||
{
|
||||
core1: 'core passed string',
|
||||
ctxFromD: {
|
||||
num: 77,
|
||||
},
|
||||
},
|
||||
'passed string',
|
||||
77
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHandler', () => {
|
||||
it('throws error if called with an unknown symbol', async () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
await expect(() =>
|
||||
contextContainer.createHandler(Symbol('unknown'), jest.fn())
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot create handler for unknown plugin: Symbol(unknown)"`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns value from original handler', async () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string>(plugins, coreId);
|
||||
|
||||
const rawHandler1 = jest.fn(() => 'handler1');
|
||||
const handler1 = contextContainer.createHandler(pluginA, rawHandler1);
|
||||
|
||||
expect(await handler1()).toEqual('handler1');
|
||||
});
|
||||
|
||||
it('passes additional arguments to handlers', async () => {
|
||||
const contextContainer = new ContextContainer<MyContext, string, [string, number]>(
|
||||
plugins,
|
||||
coreId
|
||||
);
|
||||
|
||||
const rawHandler1 = jest.fn<string, [MyContext, string, number]>(() => 'handler1');
|
||||
const handler1 = contextContainer.createHandler(pluginA, rawHandler1);
|
||||
|
||||
await handler1('passed string', 77);
|
||||
expect(rawHandler1).toHaveBeenCalledWith({}, 'passed string', 77);
|
||||
});
|
||||
});
|
||||
});
|
293
src/core/public/context/context.ts
Normal file
293
src/core/public/context/context.ts
Normal file
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { flatten } from 'lodash';
|
||||
import { pick } from '../../utils';
|
||||
import { CoreId } from '../core_system';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
|
||||
/**
|
||||
* A function that returns a context value for a specific key of given context type.
|
||||
*
|
||||
* @remarks
|
||||
* This function will be called each time a new context is built for a handler invocation.
|
||||
*
|
||||
* @param context - A partial context object containing only the keys for values provided by plugin dependencies
|
||||
* @param rest - Additional parameters provided by the service owner of this context
|
||||
* @returns The context value associated with this key. May also return a Promise which will be resolved before
|
||||
* attaching to the context object.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type IContextProvider<
|
||||
TContext extends Record<string, any>,
|
||||
TContextName extends keyof TContext,
|
||||
TProviderParameters extends any[] = []
|
||||
> = (
|
||||
context: Partial<TContext>,
|
||||
...rest: TProviderParameters
|
||||
) => Promise<TContext[TContextName]> | TContext[TContextName];
|
||||
|
||||
/**
|
||||
* A function registered by a plugin to perform some action.
|
||||
*
|
||||
* @remarks
|
||||
* A new `TContext` will be built for each handler before invoking.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type IContextHandler<TContext extends {}, TReturn, THandlerParameters extends any[] = []> = (
|
||||
context: TContext,
|
||||
...rest: THandlerParameters
|
||||
) => TReturn;
|
||||
|
||||
type Promisify<T> = T extends Promise<infer U> ? Promise<U> : Promise<T>;
|
||||
|
||||
/**
|
||||
* An object that handles registration of context providers and configuring handlers with context.
|
||||
*
|
||||
* @remarks
|
||||
* A {@link IContextContainer} 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 configuring 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 {@link IContextContainer.createHandler} function and
|
||||
* use the returned handler which will automatically build a context object when called.
|
||||
*
|
||||
* When registering context or creating handlers, the _calling plugin's opaque id_ must be provided. This id is passed
|
||||
* in via the plugin's initializer and can be accessed from the {@link PluginInitializerContext.opaqueId} Note this
|
||||
* should NOT be the context service owner's id, but the plugin that is actually registering the context or handler.
|
||||
*
|
||||
* ```ts
|
||||
* // Correct
|
||||
* class MyPlugin {
|
||||
* private readonly handlers = new Map();
|
||||
*
|
||||
* setup(core) {
|
||||
* this.contextContainer = core.context.createContextContainer();
|
||||
* return {
|
||||
* registerContext(pluginOpaqueId, contextName, provider) {
|
||||
* this.contextContainer.registerContext(pluginOpaqueId, contextName, provider);
|
||||
* },
|
||||
* registerRoute(pluginOpaqueId, path, handler) {
|
||||
* this.handlers.set(
|
||||
* path,
|
||||
* this.contextContainer.createHandler(pluginOpaqueId, handler)
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Incorrect
|
||||
* class MyPlugin {
|
||||
* private readonly handlers = new Map();
|
||||
*
|
||||
* constructor(private readonly initContext: PluginInitializerContext) {}
|
||||
*
|
||||
* setup(core) {
|
||||
* this.contextContainer = core.context.createContextContainer();
|
||||
* return {
|
||||
* registerContext(contextName, provider) {
|
||||
* // BUG!
|
||||
* // This would leak this context to all handlers rather that only plugins that depend on the calling plugin.
|
||||
* this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider);
|
||||
* },
|
||||
* registerRoute(path, handler) {
|
||||
* this.handlers.set(
|
||||
* path,
|
||||
* // BUG!
|
||||
* // This handler will not receive any contexts provided by other dependencies of the calling plugin.
|
||||
* this.contextContainer.createHandler(this.initContext.opaqueId, handler)
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface IContextContainer<
|
||||
TContext extends {},
|
||||
THandlerReturn,
|
||||
THandlerParameters extends any[] = []
|
||||
> {
|
||||
/**
|
||||
* Register a new context provider.
|
||||
*
|
||||
* @remarks
|
||||
* The value (or resolved Promise value) returned by the `provider` function will be attached to the context object
|
||||
* on the key specified by `contextName`.
|
||||
*
|
||||
* Throws an exception if more than one provider is registered for the same `contextName`.
|
||||
*
|
||||
* @param pluginOpaqueId - The plugin opaque ID for the plugin that registers this context.
|
||||
* @param contextName - The key of the `TContext` object this provider supplies the value for.
|
||||
* @param provider - A {@link IContextProvider} to be called each time a new context is created.
|
||||
* @returns The {@link IContextContainer} for method chaining.
|
||||
*/
|
||||
registerContext<TContextName extends keyof TContext>(
|
||||
pluginOpaqueId: PluginOpaqueId,
|
||||
contextName: TContextName,
|
||||
provider: IContextProvider<TContext, TContextName, THandlerParameters>
|
||||
): this;
|
||||
|
||||
/**
|
||||
* Create a new handler function pre-wired to context for the plugin.
|
||||
*
|
||||
* @param pluginOpaqueId - The plugin opaque ID for the plugin that registers this handler.
|
||||
* @param handler - Handler function to pass context object to.
|
||||
* @returns A function that takes `THandlerParameters`, calls `handler` with a new context, and returns a Promise of
|
||||
* the `handler` return value.
|
||||
*/
|
||||
createHandler(
|
||||
pluginOpaqueId: PluginOpaqueId,
|
||||
handler: IContextHandler<TContext, THandlerReturn, THandlerParameters>
|
||||
): (...rest: THandlerParameters) => Promisify<THandlerReturn>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ContextContainer<
|
||||
TContext extends Record<string, any>,
|
||||
THandlerReturn,
|
||||
THandlerParameters extends any[] = []
|
||||
> implements IContextContainer<TContext, THandlerReturn, THandlerParameters> {
|
||||
/**
|
||||
* Used to map contexts to their providers and associated plugin. In registration order which is tightly coupled to
|
||||
* plugin load order.
|
||||
*/
|
||||
private readonly contextProviders = new Map<
|
||||
keyof TContext,
|
||||
{
|
||||
provider: IContextProvider<TContext, keyof TContext, THandlerParameters>;
|
||||
source: symbol;
|
||||
}
|
||||
>();
|
||||
/** Used to keep track of which plugins registered which contexts for dependency resolution. */
|
||||
private readonly contextNamesBySource: Map<symbol, Array<keyof TContext>>;
|
||||
|
||||
/**
|
||||
* @param pluginDependencies - A map of plugins to an array of their dependencies.
|
||||
*/
|
||||
constructor(
|
||||
private readonly pluginDependencies: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>,
|
||||
private readonly coreId: CoreId
|
||||
) {
|
||||
this.contextNamesBySource = new Map<symbol, Array<keyof TContext>>([[coreId, []]]);
|
||||
}
|
||||
|
||||
public registerContext = <TContextName extends keyof TContext>(
|
||||
source: symbol,
|
||||
contextName: TContextName,
|
||||
provider: IContextProvider<TContext, TContextName, THandlerParameters>
|
||||
): this => {
|
||||
if (this.contextProviders.has(contextName)) {
|
||||
throw new Error(`Context provider for ${contextName} has already been registered.`);
|
||||
}
|
||||
if (source !== this.coreId && !this.pluginDependencies.has(source)) {
|
||||
throw new Error(`Cannot register context for unknown plugin: ${source.toString()}`);
|
||||
}
|
||||
|
||||
this.contextProviders.set(contextName, { provider, source });
|
||||
this.contextNamesBySource.set(source, [
|
||||
...(this.contextNamesBySource.get(source) || []),
|
||||
contextName,
|
||||
]);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
public createHandler = (
|
||||
source: symbol,
|
||||
handler: IContextHandler<TContext, THandlerReturn, THandlerParameters>
|
||||
) => {
|
||||
if (source !== this.coreId && !this.pluginDependencies.has(source)) {
|
||||
throw new Error(`Cannot create handler for unknown plugin: ${source.toString()}`);
|
||||
}
|
||||
|
||||
return (async (...args: THandlerParameters) => {
|
||||
const context = await this.buildContext(source, ...args);
|
||||
return handler(context, ...args);
|
||||
}) as (...args: THandlerParameters) => Promisify<THandlerReturn>;
|
||||
};
|
||||
|
||||
private async buildContext(
|
||||
source: symbol,
|
||||
...contextArgs: THandlerParameters
|
||||
): Promise<TContext> {
|
||||
const contextsToBuild: ReadonlySet<keyof TContext> = new Set(
|
||||
this.getContextNamesForSource(source)
|
||||
);
|
||||
|
||||
return [...this.contextProviders]
|
||||
.filter(([contextName]) => contextsToBuild.has(contextName))
|
||||
.reduce(
|
||||
async (contextPromise, [contextName, { provider, source: providerSource }]) => {
|
||||
const resolvedContext = await contextPromise;
|
||||
|
||||
// For the next provider, only expose the context available based on the dependencies of the plugin that
|
||||
// registered that provider.
|
||||
const exposedContext = pick(resolvedContext, [
|
||||
...this.getContextNamesForSource(providerSource),
|
||||
]);
|
||||
|
||||
return {
|
||||
...resolvedContext,
|
||||
[contextName]: await provider(exposedContext as Partial<TContext>, ...contextArgs),
|
||||
};
|
||||
},
|
||||
Promise.resolve({}) as Promise<TContext>
|
||||
);
|
||||
}
|
||||
|
||||
private getContextNamesForSource(source: symbol): ReadonlySet<keyof TContext> {
|
||||
if (source === this.coreId) {
|
||||
return this.getContextNamesForCore();
|
||||
} else {
|
||||
return this.getContextNamesForPluginId(source);
|
||||
}
|
||||
}
|
||||
|
||||
private getContextNamesForCore() {
|
||||
return new Set(this.contextNamesBySource.get(this.coreId)!);
|
||||
}
|
||||
|
||||
private getContextNamesForPluginId(pluginId: symbol) {
|
||||
// If the source is a plugin...
|
||||
const pluginDeps = this.pluginDependencies.get(pluginId);
|
||||
if (!pluginDeps) {
|
||||
// This case should never be hit, but let's be safe.
|
||||
throw new Error(`Cannot create context for unknown plugin: ${pluginId.toString()}`);
|
||||
}
|
||||
|
||||
return new Set([
|
||||
// Core contexts
|
||||
...this.contextNamesBySource.get(this.coreId)!,
|
||||
// Contexts source created
|
||||
...(this.contextNamesBySource.get(pluginId) || []),
|
||||
// Contexts sources's dependencies created
|
||||
...flatten(pluginDeps.map(p => this.contextNamesBySource.get(p) || [])),
|
||||
]);
|
||||
}
|
||||
}
|
42
src/core/public/context/context_service.mock.ts
Normal file
42
src/core/public/context/context_service.mock.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ContextService, ContextSetup } from './context_service';
|
||||
import { contextMock } from './context.mock';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<ContextSetup> = {
|
||||
createContextContainer: jest.fn().mockImplementation(() => contextMock.create()),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
type ContextServiceContract = PublicMethodsOf<ContextService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<ContextServiceContract> = {
|
||||
setup: jest.fn(),
|
||||
};
|
||||
mocked.setup.mockReturnValue(createSetupContractMock());
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const contextServiceMock = {
|
||||
create: createMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
};
|
25
src/core/public/context/context_service.test.mocks.ts
Normal file
25
src/core/public/context/context_service.test.mocks.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { contextMock } from './context.mock';
|
||||
|
||||
export const MockContextConstructor = jest.fn(contextMock.create);
|
||||
jest.doMock('./context', () => ({
|
||||
ContextContainer: MockContextConstructor,
|
||||
}));
|
36
src/core/public/context/context_service.test.ts
Normal file
36
src/core/public/context/context_service.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { MockContextConstructor } from './context_service.test.mocks';
|
||||
import { ContextService } from './context_service';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
|
||||
const pluginDependencies = new Map<PluginOpaqueId, PluginOpaqueId[]>();
|
||||
|
||||
describe('ContextService', () => {
|
||||
describe('#setup()', () => {
|
||||
test('createContextContainer returns a new container configured with pluginDependencies', () => {
|
||||
const coreId = Symbol();
|
||||
const service = new ContextService({ coreId });
|
||||
const setup = service.setup({ pluginDependencies });
|
||||
expect(setup.createContextContainer()).toBeDefined();
|
||||
expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId);
|
||||
});
|
||||
});
|
||||
});
|
117
src/core/public/context/context_service.ts
Normal file
117
src/core/public/context/context_service.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { IContextContainer, ContextContainer } from './context';
|
||||
import { CoreContext } from '../core_system';
|
||||
import { PluginOpaqueId } from '../plugins';
|
||||
|
||||
interface StartDeps {
|
||||
pluginDependencies: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ContextService {
|
||||
constructor(private readonly core: CoreContext) {}
|
||||
|
||||
public setup({ pluginDependencies }: StartDeps): ContextSetup {
|
||||
return {
|
||||
createContextContainer: <
|
||||
TContext extends {},
|
||||
THandlerReturn,
|
||||
THandlerParameters extends any[] = []
|
||||
>() =>
|
||||
new ContextContainer<TContext, THandlerReturn, THandlerParameters>(
|
||||
pluginDependencies,
|
||||
this.core.coreId
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc IContextContainer}
|
||||
*
|
||||
* @example
|
||||
* 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
|
||||
* 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<string, ((domElement: HTMLElement) => () => void)>();
|
||||
*
|
||||
* setup(core) {
|
||||
* this.contextContainer = core.createContextContainer<
|
||||
* VizRenderContext,
|
||||
* ReturnType<VizRenderer>,
|
||||
* [HTMLElement]
|
||||
* >();
|
||||
*
|
||||
* return {
|
||||
* registerContext: this.contextContainer.registerContext,
|
||||
* registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) =>
|
||||
* this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)),
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* start(core) {
|
||||
* // Register the core context available to all renderers. Use the VizRendererContext's pluginId as the first arg.
|
||||
* this.contextContainer.registerContext('viz_rendering', 'core', () => ({
|
||||
* i18n: core.i18n,
|
||||
* uiSettings: core.uiSettings
|
||||
* }));
|
||||
*
|
||||
* return {
|
||||
* registerContext: this.contextContainer.registerContext,
|
||||
*
|
||||
* renderVizualization: (renderMethod: string, domElement: HTMLElement) => {
|
||||
* if (!this.vizRenderer.has(renderMethod)) {
|
||||
* throw new Error(`Render method '${renderMethod}' has not been registered`);
|
||||
* }
|
||||
*
|
||||
* // The handler can now be called directly with only an `HTMLElement` and will automatically
|
||||
* // have a new `context` object created and populated by the context container.
|
||||
* const handler = this.vizRenderers.get(renderMethod)
|
||||
* return handler(domElement);
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ContextSetup {
|
||||
/**
|
||||
* Creates a new {@link IContextContainer} for a service owner.
|
||||
*/
|
||||
createContextContainer<
|
||||
TContext extends {},
|
||||
THandlerReturn,
|
||||
THandlerParmaters extends any[] = []
|
||||
>(): IContextContainer<TContext, THandlerReturn, THandlerParmaters>;
|
||||
}
|
21
src/core/public/context/index.ts
Normal file
21
src/core/public/context/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { ContextService, ContextSetup } from './context_service';
|
||||
export { IContextContainer, IContextProvider, IContextHandler } from './context';
|
|
@ -30,6 +30,7 @@ import { pluginsServiceMock } from './plugins/plugins_service.mock';
|
|||
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
|
||||
import { docLinksServiceMock } from './doc_links/doc_links_service.mock';
|
||||
import { renderingServiceMock } from './rendering/rendering_service.mock';
|
||||
import { contextServiceMock } from './context/context_service.mock';
|
||||
|
||||
export const MockLegacyPlatformService = legacyPlatformServiceMock.create();
|
||||
export const LegacyPlatformServiceConstructor = jest
|
||||
|
@ -120,3 +121,9 @@ export const RenderingServiceConstructor = jest.fn().mockImplementation(() => Mo
|
|||
jest.doMock('./rendering', () => ({
|
||||
RenderingService: RenderingServiceConstructor,
|
||||
}));
|
||||
|
||||
export const MockContextService = contextServiceMock.create();
|
||||
export const ContextServiceConstructor = jest.fn().mockImplementation(() => MockContextService);
|
||||
jest.doMock('./context', () => ({
|
||||
ContextService: ContextServiceConstructor,
|
||||
}));
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
MockDocLinksService,
|
||||
MockRenderingService,
|
||||
RenderingServiceConstructor,
|
||||
MockContextService,
|
||||
} from './core_system.test.mocks';
|
||||
|
||||
import { CoreSystem } from './core_system';
|
||||
|
@ -51,6 +52,7 @@ const defaultCoreSystemParams = {
|
|||
rootDomElement: document.createElement('div'),
|
||||
browserSupportsCsp: true,
|
||||
injectedMetadata: {
|
||||
uiPlugins: [],
|
||||
csp: {
|
||||
warnLegacyBrowsers: true,
|
||||
},
|
||||
|
@ -160,6 +162,11 @@ describe('#setup()', () => {
|
|||
expect(MockApplicationService.setup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls context#setup()', async () => {
|
||||
await setupCore();
|
||||
expect(MockContextService.setup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls injectedMetadata#setup()', async () => {
|
||||
await setupCore();
|
||||
expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -34,6 +34,7 @@ import { ApplicationService } from './application';
|
|||
import { mapToObject } from '../utils/';
|
||||
import { DocLinksService } from './doc_links';
|
||||
import { RenderingService } from './rendering';
|
||||
import { ContextService } from './context';
|
||||
|
||||
interface Params {
|
||||
rootDomElement: HTMLElement;
|
||||
|
@ -44,8 +45,12 @@ interface Params {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface CoreContext {}
|
||||
export type CoreId = symbol;
|
||||
|
||||
/** @internal */
|
||||
export interface CoreContext {
|
||||
coreId: CoreId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The CoreSystem is the root of the new platform, and setups all parts
|
||||
|
@ -69,6 +74,7 @@ export class CoreSystem {
|
|||
private readonly application: ApplicationService;
|
||||
private readonly docLinks: DocLinksService;
|
||||
private readonly rendering: RenderingService;
|
||||
private readonly context: ContextService;
|
||||
|
||||
private readonly rootDomElement: HTMLElement;
|
||||
private fatalErrorsSetup: FatalErrorsSetup | null = null;
|
||||
|
@ -104,8 +110,9 @@ export class CoreSystem {
|
|||
this.docLinks = new DocLinksService();
|
||||
this.rendering = new RenderingService();
|
||||
|
||||
const core: CoreContext = {};
|
||||
this.plugins = new PluginsService(core);
|
||||
const core: CoreContext = { coreId: Symbol('core') };
|
||||
this.context = new ContextService(core);
|
||||
this.plugins = new PluginsService(core, injectedMetadata.uiPlugins);
|
||||
|
||||
this.legacyPlatform = new LegacyPlatformService({
|
||||
requireLegacyFiles,
|
||||
|
@ -127,8 +134,12 @@ export class CoreSystem {
|
|||
const notifications = this.notifications.setup({ uiSettings });
|
||||
const application = this.application.setup();
|
||||
|
||||
const pluginDependencies = this.plugins.getOpaqueIds();
|
||||
const context = this.context.setup({ pluginDependencies });
|
||||
|
||||
const core: InternalCoreSetup = {
|
||||
application,
|
||||
context,
|
||||
fatalErrors: this.fatalErrorsSetup,
|
||||
http,
|
||||
injectedMetadata,
|
||||
|
|
|
@ -66,6 +66,7 @@ import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins';
|
|||
import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings';
|
||||
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
|
||||
import { DocLinksStart } from './doc_links';
|
||||
import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } from './context';
|
||||
|
||||
/** @interal */
|
||||
export { CoreContext, CoreSystem } from './core_system';
|
||||
|
@ -94,6 +95,8 @@ export {
|
|||
* https://github.com/Microsoft/web-build-tools/issues/1237
|
||||
*/
|
||||
export interface CoreSetup {
|
||||
/** {@link ContextSetup} */
|
||||
context: ContextSetup;
|
||||
/** {@link FatalErrorsSetup} */
|
||||
fatalErrors: FatalErrorsSetup;
|
||||
/** {@link HttpSetup} */
|
||||
|
@ -160,6 +163,10 @@ export {
|
|||
ChromeRecentlyAccessed,
|
||||
ChromeRecentlyAccessedHistoryItem,
|
||||
ChromeStart,
|
||||
IContextContainer,
|
||||
IContextHandler,
|
||||
IContextProvider,
|
||||
ContextSetup,
|
||||
DocLinksStart,
|
||||
ErrorToastOptions,
|
||||
FatalErrorInfo,
|
||||
|
|
|
@ -58,8 +58,10 @@ import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
|
|||
import { LegacyPlatformService } from './legacy_service';
|
||||
import { applicationServiceMock } from '../application/application_service.mock';
|
||||
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
|
||||
import { contextServiceMock } from '../context/context_service.mock';
|
||||
|
||||
const applicationSetup = applicationServiceMock.createSetupContract();
|
||||
const contextSetup = contextServiceMock.createSetupContract();
|
||||
const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract();
|
||||
const httpSetup = httpServiceMock.createSetupContract();
|
||||
const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract();
|
||||
|
@ -75,6 +77,7 @@ const defaultParams = {
|
|||
const defaultSetupDeps = {
|
||||
core: {
|
||||
application: applicationSetup,
|
||||
context: contextSetup,
|
||||
fatalErrors: fatalErrorsSetup,
|
||||
injectedMetadata: injectedMetadataSetup,
|
||||
notifications: notificationsSetup,
|
||||
|
|
|
@ -26,6 +26,7 @@ import { i18nServiceMock } from './i18n/i18n_service.mock';
|
|||
import { notificationServiceMock } from './notifications/notifications_service.mock';
|
||||
import { overlayServiceMock } from './overlays/overlay_service.mock';
|
||||
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
|
||||
import { contextServiceMock } from './context/context_service.mock';
|
||||
|
||||
export { chromeServiceMock } from './chrome/chrome_service.mock';
|
||||
export { docLinksServiceMock } from './doc_links/doc_links_service.mock';
|
||||
|
@ -40,6 +41,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
|
|||
|
||||
function createCoreSetupMock() {
|
||||
const mock: MockedKeys<CoreSetup> = {
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
notifications: notificationServiceMock.createSetupContract(),
|
||||
|
|
|
@ -18,5 +18,5 @@
|
|||
*/
|
||||
|
||||
export * from './plugins_service';
|
||||
export { Plugin, PluginInitializer } from './plugin';
|
||||
export { Plugin, PluginInitializer, PluginOpaqueId } from './plugin';
|
||||
export { PluginInitializerContext } from './plugin_context';
|
||||
|
|
|
@ -35,7 +35,8 @@ function createManifest(
|
|||
}
|
||||
|
||||
let plugin: PluginWrapper<unknown, Record<string, unknown>>;
|
||||
const initializerContext = {};
|
||||
const opaqueId = Symbol();
|
||||
const initializerContext = { opaqueId };
|
||||
const addBasePath = (path: string) => path;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -43,7 +44,7 @@ beforeEach(() => {
|
|||
mockPlugin.setup.mockClear();
|
||||
mockPlugin.start.mockClear();
|
||||
mockPlugin.stop.mockClear();
|
||||
plugin = new PluginWrapper(createManifest('plugin-a'), initializerContext);
|
||||
plugin = new PluginWrapper(createManifest('plugin-a'), opaqueId, initializerContext);
|
||||
});
|
||||
|
||||
describe('PluginWrapper', () => {
|
||||
|
|
|
@ -22,6 +22,9 @@ import { PluginInitializerContext } from './plugin_context';
|
|||
import { loadPluginBundle } from './plugin_loader';
|
||||
import { CoreStart, CoreSetup } from '..';
|
||||
|
||||
/** @public */
|
||||
export type PluginOpaqueId = symbol;
|
||||
|
||||
/**
|
||||
* The interface that should be returned by a `PluginInitializer`.
|
||||
*
|
||||
|
@ -72,6 +75,7 @@ export class PluginWrapper<
|
|||
|
||||
constructor(
|
||||
readonly discoveredPlugin: DiscoveredPlugin,
|
||||
public readonly opaqueId: PluginOpaqueId,
|
||||
private readonly initializerContext: PluginInitializerContext
|
||||
) {
|
||||
this.name = discoveredPlugin.id;
|
||||
|
|
|
@ -21,7 +21,7 @@ import { omit } from 'lodash';
|
|||
|
||||
import { DiscoveredPlugin } from '../../server';
|
||||
import { CoreContext } from '../core_system';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginWrapper, PluginOpaqueId } from './plugin';
|
||||
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
|
||||
import { CoreSetup, CoreStart } from '../';
|
||||
|
||||
|
@ -30,8 +30,12 @@ import { CoreSetup, CoreStart } from '../';
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PluginInitializerContext {}
|
||||
export interface PluginInitializerContext {
|
||||
/**
|
||||
* A symbol used to identify this plugin in the system. Needed when registering handlers or context providers.
|
||||
*/
|
||||
readonly opaqueId: PluginOpaqueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a plugin-specific context passed to the plugin's construtor. This is currently
|
||||
|
@ -43,9 +47,12 @@ export interface PluginInitializerContext {}
|
|||
*/
|
||||
export function createPluginInitializerContext(
|
||||
coreContext: CoreContext,
|
||||
opaqueId: PluginOpaqueId,
|
||||
pluginManifest: DiscoveredPlugin
|
||||
): PluginInitializerContext {
|
||||
return {};
|
||||
return {
|
||||
opaqueId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,8 +76,9 @@ export function createPluginSetupContext<
|
|||
plugin: PluginWrapper<TSetup, TStart, TPluginsSetup, TPluginsStart>
|
||||
): CoreSetup {
|
||||
return {
|
||||
http: deps.http,
|
||||
context: omit(deps.context, 'setCurrentPlugin'),
|
||||
fatalErrors: deps.fatalErrors,
|
||||
http: deps.http,
|
||||
notifications: deps.notifications,
|
||||
uiSettings: deps.uiSettings,
|
||||
};
|
||||
|
|
|
@ -38,6 +38,7 @@ const createStartContractMock = () => {
|
|||
type PluginsServiceContract = PublicMethodsOf<PluginsService>;
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<PluginsServiceContract> = {
|
||||
getOpaqueIds: jest.fn(),
|
||||
setup: jest.fn(),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
mockPluginInitializerProvider,
|
||||
} from './plugins_service.test.mocks';
|
||||
|
||||
import { PluginName } from 'src/core/server';
|
||||
import { PluginName, DiscoveredPlugin } from 'src/core/server';
|
||||
import { CoreContext } from '../core_system';
|
||||
import {
|
||||
PluginsService,
|
||||
|
@ -43,6 +43,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad
|
|||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { CoreSetup, CoreStart } from '..';
|
||||
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
|
||||
import { contextServiceMock } from '../context/context_service.mock';
|
||||
|
||||
export let mockPluginInitializers: Map<PluginName, MockedPluginInitializer>;
|
||||
|
||||
|
@ -50,35 +51,37 @@ mockPluginInitializerProvider.mockImplementation(
|
|||
pluginName => mockPluginInitializers.get(pluginName)!
|
||||
);
|
||||
|
||||
let plugins: Array<{ id: string; plugin: DiscoveredPlugin }>;
|
||||
|
||||
type DeeplyMocked<T> = { [P in keyof T]: jest.Mocked<T[P]> };
|
||||
|
||||
const mockCoreContext: CoreContext = {};
|
||||
const mockCoreContext: CoreContext = { coreId: Symbol() };
|
||||
let mockSetupDeps: DeeplyMocked<PluginsServiceSetupDeps>;
|
||||
let mockSetupContext: DeeplyMocked<CoreSetup>;
|
||||
let mockStartDeps: DeeplyMocked<PluginsServiceStartDeps>;
|
||||
let mockStartContext: DeeplyMocked<CoreStart>;
|
||||
|
||||
beforeEach(() => {
|
||||
plugins = [
|
||||
{ id: 'pluginA', plugin: createManifest('pluginA') },
|
||||
{ id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) },
|
||||
{
|
||||
id: 'pluginC',
|
||||
plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }),
|
||||
},
|
||||
];
|
||||
mockSetupDeps = {
|
||||
application: applicationServiceMock.createSetupContract(),
|
||||
injectedMetadata: (function() {
|
||||
const metadata = injectedMetadataServiceMock.createSetupContract();
|
||||
metadata.getPlugins.mockReturnValue([
|
||||
{ id: 'pluginA', plugin: createManifest('pluginA') },
|
||||
{ id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) },
|
||||
{
|
||||
id: 'pluginC',
|
||||
plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }),
|
||||
},
|
||||
]);
|
||||
return metadata;
|
||||
})(),
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
|
||||
http: httpServiceMock.createSetupContract(),
|
||||
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
|
||||
notifications: notificationServiceMock.createSetupContract(),
|
||||
uiSettings: uiSettingsServiceMock.createSetupContract(),
|
||||
};
|
||||
mockSetupContext = omit(mockSetupDeps, 'application', 'injectedMetadata');
|
||||
mockSetupContext = {
|
||||
...omit(mockSetupDeps, 'application', 'injectedMetadata'),
|
||||
};
|
||||
mockStartDeps = {
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
docLinks: docLinksServiceMock.createStartContract(),
|
||||
|
@ -148,10 +151,25 @@ function createManifest(
|
|||
};
|
||||
}
|
||||
|
||||
test('`PluginsService.getOpaqueIds` returns dependency tree of symbols', () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
Symbol(pluginA) => Array [],
|
||||
Symbol(pluginB) => Array [
|
||||
Symbol(pluginA),
|
||||
],
|
||||
Symbol(pluginC) => Array [
|
||||
Symbol(pluginA),
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => {
|
||||
mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle'));
|
||||
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Could not load bundle"`
|
||||
);
|
||||
|
@ -159,14 +177,14 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () =>
|
|||
|
||||
test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => {
|
||||
mockPluginInitializers.set('pluginA', (() => ({})) as any);
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."`
|
||||
);
|
||||
});
|
||||
|
||||
test('`PluginsService.setup` calls loadPluginBundles with http and plugins', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3);
|
||||
|
@ -175,17 +193,17 @@ test('`PluginsService.setup` calls loadPluginBundles with http and plugins', asy
|
|||
expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginC');
|
||||
});
|
||||
|
||||
test('`PluginsService.setup` initalizes plugins with CoreContext', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
test('`PluginsService.setup` initalizes plugins with PluginIntitializerContext', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(mockCoreContext);
|
||||
expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(mockCoreContext);
|
||||
expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(mockCoreContext);
|
||||
expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(expect.any(Object));
|
||||
expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
|
||||
|
@ -203,15 +221,13 @@ test('`PluginsService.setup` exposes dependent setup contracts to plugins', asyn
|
|||
});
|
||||
|
||||
test('`PluginsService.setup` does not set missing dependent setup contracts', async () => {
|
||||
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
|
||||
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
|
||||
]);
|
||||
plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }];
|
||||
mockPluginInitializers.set('pluginD', jest.fn(() => ({
|
||||
setup: jest.fn(),
|
||||
start: jest.fn(),
|
||||
})) as any);
|
||||
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
// If a dependency is missing it should not be in the deps at all, not even as undefined.
|
||||
|
@ -222,7 +238,7 @@ test('`PluginsService.setup` does not set missing dependent setup contracts', as
|
|||
});
|
||||
|
||||
test('`PluginsService.setup` returns plugin setup contracts', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
const { contracts } = await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
// Verify that plugin contracts were available
|
||||
|
@ -231,7 +247,7 @@ test('`PluginsService.setup` returns plugin setup contracts', async () => {
|
|||
});
|
||||
|
||||
test('`PluginsService.start` exposes dependent start contracts to plugins', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
await pluginsService.start(mockStartDeps);
|
||||
|
||||
|
@ -250,15 +266,13 @@ test('`PluginsService.start` exposes dependent start contracts to plugins', asyn
|
|||
});
|
||||
|
||||
test('`PluginsService.start` does not set missing dependent start contracts', async () => {
|
||||
mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([
|
||||
{ id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) },
|
||||
]);
|
||||
plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }];
|
||||
mockPluginInitializers.set('pluginD', jest.fn(() => ({
|
||||
setup: jest.fn(),
|
||||
start: jest.fn(),
|
||||
})) as any);
|
||||
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
await pluginsService.start(mockStartDeps);
|
||||
|
||||
|
@ -270,7 +284,7 @@ test('`PluginsService.start` does not set missing dependent start contracts', as
|
|||
});
|
||||
|
||||
test('`PluginsService.start` returns plugin start contracts', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
const { contracts } = await pluginsService.start(mockStartDeps);
|
||||
|
||||
|
@ -280,7 +294,7 @@ test('`PluginsService.start` returns plugin start contracts', async () => {
|
|||
});
|
||||
|
||||
test('`PluginService.stop` calls the stop function on each plugin', async () => {
|
||||
const pluginsService = new PluginsService(mockCoreContext);
|
||||
const pluginsService = new PluginsService(mockCoreContext, plugins);
|
||||
await pluginsService.setup(mockSetupDeps);
|
||||
|
||||
const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value;
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginName } from '../../server';
|
||||
import { DiscoveredPlugin, PluginName } from '../../server';
|
||||
import { CoreService } from '../../types';
|
||||
import { CoreContext } from '../core_system';
|
||||
import { PluginWrapper } from './plugin';
|
||||
import { PluginWrapper, PluginOpaqueId } from './plugin';
|
||||
import {
|
||||
createPluginInitializerContext,
|
||||
createPluginSetupContext,
|
||||
|
@ -35,11 +35,11 @@ export type PluginsServiceStartDeps = InternalCoreStart;
|
|||
|
||||
/** @internal */
|
||||
export interface PluginsServiceSetup {
|
||||
contracts: Map<string, unknown>;
|
||||
contracts: ReadonlyMap<string, unknown>;
|
||||
}
|
||||
/** @internal */
|
||||
export interface PluginsServiceStart {
|
||||
contracts: Map<string, unknown>;
|
||||
contracts: ReadonlyMap<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,37 +50,56 @@ export interface PluginsServiceStart {
|
|||
*/
|
||||
export class PluginsService implements CoreService<PluginsServiceSetup, PluginsServiceStart> {
|
||||
/** Plugin wrappers in topological order. */
|
||||
private readonly plugins: Map<
|
||||
PluginName,
|
||||
PluginWrapper<unknown, Record<string, unknown>>
|
||||
> = new Map();
|
||||
private readonly plugins = new Map<PluginName, PluginWrapper<unknown, Record<string, unknown>>>();
|
||||
private readonly pluginDependencies = new Map<PluginName, PluginName[]>();
|
||||
|
||||
private readonly satupPlugins: PluginName[] = [];
|
||||
|
||||
constructor(private readonly coreContext: CoreContext) {}
|
||||
constructor(
|
||||
private readonly coreContext: CoreContext,
|
||||
plugins: Array<{ id: PluginName; plugin: DiscoveredPlugin }>
|
||||
) {
|
||||
// Generate opaque ids
|
||||
const opaqueIds = new Map<PluginName, PluginOpaqueId>(plugins.map(p => [p.id, Symbol(p.id)]));
|
||||
|
||||
public async setup(deps: PluginsServiceSetupDeps) {
|
||||
// Construct plugin wrappers, depending on the topological order set by the server.
|
||||
deps.injectedMetadata
|
||||
.getPlugins()
|
||||
.forEach(({ id, plugin }) =>
|
||||
this.plugins.set(
|
||||
id,
|
||||
new PluginWrapper(plugin, createPluginInitializerContext(deps, plugin))
|
||||
// Setup dependency map and plugin wrappers
|
||||
plugins.forEach(({ id, plugin }) => {
|
||||
// Setup map of dependencies
|
||||
this.pluginDependencies.set(id, [
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter(optPlugin => opaqueIds.has(optPlugin)),
|
||||
]);
|
||||
|
||||
// Construct plugin wrappers, depending on the topological order set by the server.
|
||||
this.plugins.set(
|
||||
id,
|
||||
new PluginWrapper(
|
||||
plugin,
|
||||
opaqueIds.get(id)!,
|
||||
createPluginInitializerContext(this.coreContext, opaqueIds.get(id)!, plugin)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getOpaqueIds(): ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]> {
|
||||
// Return dependency map of opaque ids
|
||||
return new Map(
|
||||
[...this.pluginDependencies].map(([id, deps]) => [
|
||||
this.plugins.get(id)!.opaqueId,
|
||||
deps.map(depId => this.plugins.get(depId)!.opaqueId),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public async setup(deps: PluginsServiceSetupDeps): Promise<PluginsServiceSetup> {
|
||||
// Load plugin bundles
|
||||
await this.loadPluginBundles(deps.http.basePath.prepend);
|
||||
|
||||
// Setup each plugin with required and optional plugin contracts
|
||||
const contracts = new Map<string, unknown>();
|
||||
for (const [pluginName, plugin] of this.plugins.entries()) {
|
||||
const pluginDeps = new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)),
|
||||
]);
|
||||
|
||||
const pluginDepContracts = [...pluginDeps.keys()].reduce(
|
||||
const pluginDepContracts = [...this.pluginDependencies.get(pluginName)!].reduce(
|
||||
(depContracts, dependencyName) => {
|
||||
// Only set if present. Could be absent if plugin does not have client-side code or is a
|
||||
// missing optional plugin.
|
||||
|
@ -108,16 +127,11 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
|
|||
return { contracts };
|
||||
}
|
||||
|
||||
public async start(deps: PluginsServiceStartDeps) {
|
||||
public async start(deps: PluginsServiceStartDeps): Promise<PluginsServiceStart> {
|
||||
// Setup each plugin with required and optional plugin contracts
|
||||
const contracts = new Map<string, unknown>();
|
||||
for (const [pluginName, plugin] of this.plugins.entries()) {
|
||||
const pluginDeps = new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)),
|
||||
]);
|
||||
|
||||
const pluginDepContracts = [...pluginDeps.keys()].reduce(
|
||||
const pluginDepContracts = [...this.pluginDependencies.get(pluginName)!].reduce(
|
||||
(depContracts, dependencyName) => {
|
||||
// Only set if present. Could be absent if plugin does not have client-side code or is a
|
||||
// missing optional plugin.
|
||||
|
|
|
@ -167,12 +167,23 @@ export interface ChromeStart {
|
|||
setIsVisible(isVisible: boolean): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ContextSetup {
|
||||
createContextContainer<TContext extends {}, THandlerReturn, THandlerParmaters extends any[] = []>(): IContextContainer<TContext, THandlerReturn, THandlerParmaters>;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export interface CoreContext {
|
||||
// Warning: (ae-forgotten-export) The symbol "CoreId" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
coreId: CoreId;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface CoreSetup {
|
||||
// (undocumented)
|
||||
context: ContextSetup;
|
||||
// (undocumented)
|
||||
fatalErrors: FatalErrorsSetup;
|
||||
// (undocumented)
|
||||
|
@ -477,6 +488,20 @@ export interface I18nStart {
|
|||
}) => JSX.Element;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface IContextContainer<TContext extends {}, THandlerReturn, THandlerParameters extends any[] = []> {
|
||||
// Warning: (ae-forgotten-export) The symbol "Promisify" needs to be exported by the entry point index.d.ts
|
||||
createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler<TContext, THandlerReturn, THandlerParameters>): (...rest: THandlerParameters) => Promisify<THandlerReturn>;
|
||||
// Warning: (ae-forgotten-export) The symbol "PluginOpaqueId" needs to be exported by the entry point index.d.ts
|
||||
registerContext<TContextName extends keyof TContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider<TContext, TContextName, THandlerParameters>): this;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type IContextHandler<TContext extends {}, TReturn, THandlerParameters extends any[] = []> = (context: TContext, ...rest: THandlerParameters) => TReturn;
|
||||
|
||||
// @public
|
||||
export type IContextProvider<TContext extends Record<string, any>, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial<TContext>, ...rest: TProviderParameters) => Promise<TContext[TContextName]> | TContext[TContextName];
|
||||
|
||||
// @internal (undocumented)
|
||||
export interface InternalCoreSetup extends CoreSetup {
|
||||
// (undocumented)
|
||||
|
@ -567,6 +592,7 @@ export type PluginInitializer<TSetup, TStart, TPluginsSetup extends object = obj
|
|||
|
||||
// @public
|
||||
export interface PluginInitializerContext {
|
||||
readonly opaqueId: PluginOpaqueId;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function mapToObject<V = unknown>(map: Map<string, V>) {
|
||||
export function mapToObject<V = unknown>(map: ReadonlyMap<string, V>) {
|
||||
const result: Record<string, V> = Object.create(null);
|
||||
for (const [key, value] of map) {
|
||||
result[key] = value;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue