mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Presentation Util] Remove the plugin services toolkit (#195502)
Closes https://github.com/elastic/kibana/issues/194199 ## Summary Now that no plugins use anything from the `PresentationUtil` services toolkit, it is safe to remove all code and documentation related to this from the `PresentationUtil` plugin. ### Checklist - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
38d0bdd3de
commit
1053493c9c
9 changed files with 6 additions and 836 deletions
|
@ -12,208 +12,12 @@ related: []
|
|||
|
||||
The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).
|
||||
|
||||
## Plugin Services Toolkit
|
||||
|
||||
While Kibana provides a `useKibana` hook for use in a plugin, the number of services it provides is very large. This presents a set of difficulties:
|
||||
|
||||
- a direct dependency upon the Kibana environment;
|
||||
- a requirement to mock the full Kibana environment when testing or using Storybook;
|
||||
- a lack of knowledge as to what services are being consumed at any given time.
|
||||
|
||||
To mitigate these difficulties, the Presentation Team creates services within the plugin that then consume Kibana-provided (or other) services. This is a toolkit for creating simple services within a plugin.
|
||||
|
||||
### Overview
|
||||
|
||||
- A `PluginServiceFactory` is a function that will return a set of functions-- which comprise a `Service`-- given a set of parameters.
|
||||
- A `PluginServiceProvider` is an object that use a factory to start, stop or provide a `Service`.
|
||||
- A `PluginServiceRegistry` is a collection of providers for a given environment, (e.g. Kibana, Jest, Storybook, stub, etc).
|
||||
- A `PluginServices` object uses a registry to provide services throughout the plugin.
|
||||
|
||||
### Defining Services
|
||||
|
||||
To start, a plugin should define a set of services it wants to provide to itself or other plugins.
|
||||
|
||||
<DocAccordion buttonContent="Service Definition Example" initialIsOpen>
|
||||
```ts
|
||||
export interface PresentationDashboardsService {
|
||||
findDashboards: (
|
||||
query: string,
|
||||
fields: string[]
|
||||
) => Promise<Array<SimpleSavedObject<DashboardSavedObject>>>;
|
||||
findDashboardsByTitle: (title: string) => Promise<Array<SimpleSavedObject<DashboardSavedObject>>>;
|
||||
}
|
||||
|
||||
export interface PresentationFooService {
|
||||
getFoo: () => string;
|
||||
setFoo: (bar: string) => void;
|
||||
}
|
||||
|
||||
export interface PresentationUtilServices {
|
||||
dashboards: PresentationDashboardsService;
|
||||
foo: PresentationFooService;
|
||||
}
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
This definition will be used in the toolkit to ensure services are complete and as expected.
|
||||
|
||||
### Plugin Services
|
||||
|
||||
The `PluginServices` class hosts a registry of service providers from which a plugin can access its services. It uses the service definition as a generic.
|
||||
|
||||
```ts
|
||||
export const pluginServices = new PluginServices<PresentationUtilServices>();
|
||||
```
|
||||
|
||||
This can be placed in the `index.ts` file of a `services` directory within your plugin.
|
||||
|
||||
Once created, it simply requires a `PluginServiceRegistry` to be started and set.
|
||||
|
||||
### Service Provider Registry
|
||||
|
||||
Each environment in which components are used requires a `PluginServiceRegistry` to specify how the providers are started. For example, simple stubs of services require no parameters to start, (so the `StartParameters` generic remains unspecified)
|
||||
|
||||
<DocAccordion buttonContent="Stubbed Service Registry Example" initialIsOpen>
|
||||
```ts
|
||||
export const providers: PluginServiceProviders<PresentationUtilServices> = {
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
foo: new PluginServiceProvider(fooServiceFactory),
|
||||
};
|
||||
|
||||
export const serviceRegistry = new PluginServiceRegistry<PresentationUtilServices>(providers);
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
By contrast, a registry that uses Kibana can provide `KibanaPluginServiceParams` to determine how to start its providers, so the `StartParameters` generic is given:
|
||||
|
||||
<DocAccordion buttonContent="Kibana Service Registry Example" initialIsOpen>
|
||||
```ts
|
||||
export const providers: PluginServiceProviders<
|
||||
PresentationUtilServices,
|
||||
KibanaPluginServiceParams<PresentationUtilPluginStart>
|
||||
> = {
|
||||
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
|
||||
foo: new PluginServiceProvider(fooServiceFactory),
|
||||
};
|
||||
|
||||
export const serviceRegistry = new PluginServiceRegistry<
|
||||
PresentationUtilServices,
|
||||
KibanaPluginServiceParams<PresentationUtilPluginStart>
|
||||
>(providers);
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
### Service Provider
|
||||
|
||||
A `PluginServiceProvider` is a container for a Service Factory that is responsible for starting, stopping and providing a service implementation. A Service Provider doesn't change, rather the factory and the relevant `StartParameters` change.
|
||||
|
||||
### Service Factories
|
||||
|
||||
A Service Factory is nothing more than a function that uses `StartParameters` to return a set of functions that conforms to a portion of the `Services` specification. For each service, a factory is provided for each environment.
|
||||
|
||||
Given a service definition:
|
||||
|
||||
```ts
|
||||
export interface PresentationFooService {
|
||||
getFoo: () => string;
|
||||
setFoo: (bar: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
a factory for a stubbed version might look like this:
|
||||
|
||||
```ts
|
||||
type FooServiceFactory = PluginServiceFactory<PresentationFooService>;
|
||||
|
||||
export const fooServiceFactory: FooServiceFactory = () => ({
|
||||
getFoo: () => 'bar',
|
||||
setFoo: (bar) => { console.log(`${bar} set!`)},
|
||||
});
|
||||
```
|
||||
|
||||
and a factory for a Kibana version might look like this:
|
||||
|
||||
```ts
|
||||
export type FooServiceFactory = KibanaPluginServiceFactory<
|
||||
PresentationFooService,
|
||||
PresentationUtilPluginStart
|
||||
>;
|
||||
|
||||
export const fooServiceFactory: FooServiceFactory = ({
|
||||
coreStart,
|
||||
startPlugins,
|
||||
}) => {
|
||||
// ...do something with Kibana services...
|
||||
|
||||
return {
|
||||
getFoo: //...
|
||||
setFoo: //...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Services
|
||||
|
||||
Once your services and providers are defined, and you have at least one set of factories, you can use `PluginServices` to provide the services to your React components:
|
||||
|
||||
<DocAccordion buttonContent="Services starting in a plugin" initialIsOpen>
|
||||
```ts
|
||||
// plugin.ts
|
||||
import { pluginServices } from './services';
|
||||
import { registry } from './services/kibana';
|
||||
|
||||
public start(
|
||||
coreStart: CoreStart,
|
||||
startPlugins: StartDeps
|
||||
): Promise<PresentationUtilPluginStart> {
|
||||
pluginServices.setRegistry(registry.start({ coreStart, startPlugins }));
|
||||
return {};
|
||||
}
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
and wrap your root React component with the `PluginServices` context:
|
||||
|
||||
<DocAccordion buttonContent="Providing services in a React context" initialIsOpen>
|
||||
```ts
|
||||
import { pluginServices } from './services';
|
||||
|
||||
const ContextProvider = pluginServices.getContextProvider(),
|
||||
|
||||
return(
|
||||
<I18nContext>
|
||||
<WhateverElse>
|
||||
<ContextProvider>{application}</ContextProvider>
|
||||
</WhateverElse>
|
||||
</I18nContext>
|
||||
)
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
and then, consume your services using provided hooks in a component:
|
||||
|
||||
<DocAccordion buttonContent="Consuming services in a component" initialIsOpen>
|
||||
```ts
|
||||
// component.ts
|
||||
|
||||
import { pluginServices } from '../services';
|
||||
|
||||
export function MyComponent() {
|
||||
// Retrieve all context hooks from `PluginServices`, destructuring for the one we're using
|
||||
const { foo } = pluginServices.getHooks();
|
||||
|
||||
// Use the `useContext` hook to access the API.
|
||||
const { getFoo } = foo.useService();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
</DocAccordion>
|
||||
|
||||
## Redux Embeddables
|
||||
|
||||
The Redux Embeddables system allows embeddable authors to interact with their embeddables in a standardized way using Redux toolkit. This wrapper abstracts away store and slice creation, and embeddable input sync. To use this system, a developer can use CreateReduxEmbeddableTools in the constructor of their embeddable, supplying a collection of reducers.
|
||||
|
||||
### Reducers
|
||||
|
||||
The reducer object expected by the ReduxEmbeddableWrapper is the same type as the reducers expected by [Redux Toolkit's CreateSlice](https://redux-toolkit.js.org/api/createslice).
|
||||
|
||||
<DocAccordion buttonContent="Reducers Example" initialIsOpen>
|
||||
|
@ -247,6 +51,8 @@ From components under the embeddable, actions, containerActions, and the current
|
|||
// change specialBoolean after 5 seconds
|
||||
setTimeout(() => embeddableInstance.dispatch.setSpecialBoolean(false), 5000);
|
||||
|
||||
}
|
||||
```
|
||||
}
|
||||
|
||||
```
|
||||
</DocAccordion>
|
||||
```
|
||||
|
|
|
@ -12,14 +12,6 @@ import { PresentationUtilPlugin } from './plugin';
|
|||
|
||||
export type { PresentationLabsService } from './services/presentation_labs_service';
|
||||
|
||||
export type {
|
||||
KibanaPluginServiceFactory,
|
||||
PluginServiceFactory,
|
||||
PluginServiceProviders,
|
||||
KibanaPluginServiceParams,
|
||||
} from './services/create';
|
||||
export { PluginServices, PluginServiceProvider, PluginServiceRegistry } from './services/create';
|
||||
|
||||
export type { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types';
|
||||
export type { SaveModalDashboardProps } from './components/types';
|
||||
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { DependencyManager } from './dependency_manager';
|
||||
|
||||
describe('DependencyManager', () => {
|
||||
it('orderDependencies. Should sort topology by dependencies', () => {
|
||||
const graph = {
|
||||
N: [],
|
||||
R: [],
|
||||
A: ['B', 'C'],
|
||||
B: ['D'],
|
||||
C: ['F', 'B'],
|
||||
F: ['E'],
|
||||
E: ['D'],
|
||||
D: ['L'],
|
||||
};
|
||||
const sortedTopology = ['N', 'R', 'L', 'D', 'B', 'E', 'F', 'C', 'A'];
|
||||
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
|
||||
});
|
||||
|
||||
it('should include final vertex if it has dependencies', () => {
|
||||
const graph = {
|
||||
A: [],
|
||||
B: [],
|
||||
C: ['A', 'B'],
|
||||
};
|
||||
const sortedTopology = ['A', 'B', 'C'];
|
||||
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
|
||||
});
|
||||
|
||||
it('orderDependencies. Should return base topology if no depended vertices', () => {
|
||||
const graph = {
|
||||
N: [],
|
||||
R: [],
|
||||
D: undefined,
|
||||
};
|
||||
const sortedTopology = ['N', 'R', 'D'];
|
||||
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
|
||||
});
|
||||
|
||||
describe('circular dependencies', () => {
|
||||
it('should detect circular dependencies and throw error with path', () => {
|
||||
const graph = {
|
||||
N: ['R'],
|
||||
R: ['A'],
|
||||
A: ['B'],
|
||||
B: ['C'],
|
||||
C: ['D'],
|
||||
D: ['E'],
|
||||
E: ['F'],
|
||||
F: ['L'],
|
||||
L: ['G'],
|
||||
G: ['N'],
|
||||
};
|
||||
const circularPath = ['G', 'L', 'F', 'E', 'D', 'C', 'B', 'A', 'R', 'N'].join(' -> ');
|
||||
const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`;
|
||||
|
||||
expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage);
|
||||
});
|
||||
|
||||
it('should detect circular dependency if circular reference is the first dependency for a vertex', () => {
|
||||
const graph = {
|
||||
A: ['B'],
|
||||
B: ['A', 'C'],
|
||||
C: [],
|
||||
};
|
||||
|
||||
expect(() => DependencyManager.orderDependencies(graph)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
type GraphVertex = string | number | symbol;
|
||||
type Graph<T extends GraphVertex = GraphVertex> = Record<T, T[] | null | undefined>;
|
||||
type BreadCrumbs = Record<GraphVertex, boolean>;
|
||||
|
||||
interface CycleDetectionResult<T extends GraphVertex = GraphVertex> {
|
||||
hasCycle: boolean;
|
||||
path: T[];
|
||||
}
|
||||
|
||||
export class DependencyManager {
|
||||
static orderDependencies<T extends GraphVertex = GraphVertex>(graph: Graph<T>) {
|
||||
const cycleInfo = DependencyManager.getSortedDependencies(graph);
|
||||
if (cycleInfo.hasCycle) {
|
||||
const error = DependencyManager.getCyclePathError(cycleInfo.path);
|
||||
DependencyManager.throwCyclicPathError(error);
|
||||
}
|
||||
|
||||
return cycleInfo.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* DFS algorithm for checking if graph is a DAG (Directed Acyclic Graph)
|
||||
* and sorting topogy (dependencies) if graph is DAG.
|
||||
* @param {Graph} graph - graph of dependencies.
|
||||
*/
|
||||
private static getSortedDependencies<T extends GraphVertex = GraphVertex>(
|
||||
graph: Graph<T> = {} as Graph<T>
|
||||
): CycleDetectionResult<T> {
|
||||
const sortedVertices: Set<T> = new Set();
|
||||
const vertices = Object.keys(graph) as T[];
|
||||
return vertices.reduce<CycleDetectionResult<T>>((cycleInfo, srcVertex) => {
|
||||
if (cycleInfo.hasCycle) {
|
||||
return cycleInfo;
|
||||
}
|
||||
|
||||
return DependencyManager.sortVerticesFrom(
|
||||
srcVertex,
|
||||
graph,
|
||||
sortedVertices,
|
||||
{},
|
||||
{},
|
||||
cycleInfo
|
||||
);
|
||||
}, DependencyManager.createCycleInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified DFS algorithm for topological sort.
|
||||
* @param {T extends GraphVertex} srcVertex - a source vertex - the start point of dependencies ordering.
|
||||
* @param {Graph<T extends GraphVertex>} graph - graph of dependencies, represented in the adjacency list form.
|
||||
* @param {Set<GraphVertex>} sortedVertices - ordered dependencies path from the free to the dependent vertex.
|
||||
* @param {BreadCrumbs} visited - record of visited vertices.
|
||||
* @param {BreadCrumbs} inpath - record of vertices, which was met in the path. Is used for detecting cycles.
|
||||
*/
|
||||
private static sortVerticesFrom<T extends GraphVertex = GraphVertex>(
|
||||
srcVertex: T,
|
||||
graph: Graph<T>,
|
||||
sortedVertices: Set<T>,
|
||||
visited: BreadCrumbs = {},
|
||||
inpath: BreadCrumbs = {},
|
||||
cycle: CycleDetectionResult<T>
|
||||
): CycleDetectionResult<T> {
|
||||
visited[srcVertex] = true;
|
||||
inpath[srcVertex] = true;
|
||||
|
||||
const vertexEdges =
|
||||
graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex];
|
||||
|
||||
cycle = vertexEdges!.reduce<CycleDetectionResult<T>>((info, vertex) => {
|
||||
if (inpath[vertex]) {
|
||||
return { ...info, hasCycle: true };
|
||||
} else if (!visited[vertex]) {
|
||||
return DependencyManager.sortVerticesFrom(
|
||||
vertex,
|
||||
graph,
|
||||
sortedVertices,
|
||||
visited,
|
||||
inpath,
|
||||
info
|
||||
);
|
||||
}
|
||||
return info;
|
||||
}, cycle);
|
||||
|
||||
inpath[srcVertex] = false;
|
||||
|
||||
if (!sortedVertices.has(srcVertex)) {
|
||||
sortedVertices.add(srcVertex);
|
||||
}
|
||||
|
||||
return {
|
||||
...cycle,
|
||||
path: [...sortedVertices],
|
||||
};
|
||||
}
|
||||
|
||||
private static createCycleInfo<T extends GraphVertex = GraphVertex>(
|
||||
path: T[] = [],
|
||||
hasCycle: boolean = false
|
||||
): CycleDetectionResult<T> {
|
||||
return { hasCycle, path };
|
||||
}
|
||||
|
||||
private static getCyclePathError<T extends GraphVertex = GraphVertex>(
|
||||
cyclePath: CycleDetectionResult<T>['path']
|
||||
) {
|
||||
const cycleString = cyclePath.join(' -> ');
|
||||
return `Circular dependency detected while setting up services: ${cycleString}`;
|
||||
}
|
||||
|
||||
private static throwCyclicPathError(error: string) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { CoreStart, AppUpdater, PluginInitializerContext } from '@kbn/core/public';
|
||||
|
||||
/**
|
||||
* A factory function for creating a service.
|
||||
*
|
||||
* The `Service` generic determines the shape of the API being produced.
|
||||
* The `StartParameters` generic determines what parameters are expected to
|
||||
* create the service.
|
||||
*/
|
||||
export type PluginServiceFactory<Service, Parameters = {}, RequiredServices = {}> = (
|
||||
params: Parameters,
|
||||
requiredServices: RequiredServices
|
||||
) => Service;
|
||||
|
||||
/**
|
||||
* Parameters necessary to create a Kibana-based service, (e.g. during Plugin
|
||||
* startup or setup).
|
||||
*
|
||||
* The `Start` generic refers to the specific Plugin `TPluginsStart`.
|
||||
*/
|
||||
export interface KibanaPluginServiceParams<Start extends {}> {
|
||||
coreStart: CoreStart;
|
||||
startPlugins: Start;
|
||||
appUpdater?: BehaviorSubject<AppUpdater>;
|
||||
initContext?: PluginInitializerContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function for creating a Kibana-based service.
|
||||
*
|
||||
* The `Service` generic determines the shape of the API being produced.
|
||||
* The `Setup` generic refers to the specific Plugin `TPluginsSetup`.
|
||||
* The `Start` generic refers to the specific Plugin `TPluginsStart`.
|
||||
*/
|
||||
export type KibanaPluginServiceFactory<Service, Start extends {}, RequiredServices = {}> = (
|
||||
params: KibanaPluginServiceParams<Start>,
|
||||
requiredServices: RequiredServices
|
||||
) => Service;
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PluginServiceRegistry } from './registry';
|
||||
|
||||
export { PluginServiceRegistry } from './registry';
|
||||
export type { PluginServiceProviders } from './provider';
|
||||
export { PluginServiceProvider } from './provider';
|
||||
export type {
|
||||
PluginServiceFactory,
|
||||
KibanaPluginServiceFactory,
|
||||
KibanaPluginServiceParams,
|
||||
} from './factory';
|
||||
|
||||
type ServiceHooks<Services> = { [K in keyof Services]: { useService: () => Services[K] } };
|
||||
|
||||
/**
|
||||
* `PluginServices` is a top-level class for specifying and accessing services within a plugin.
|
||||
*
|
||||
* A `PluginServices` object can be provided with a `PluginServiceRegistry` at any time, which will
|
||||
* then be used to provide services to any component that accesses it.
|
||||
*
|
||||
* The `Services` generic determines the shape of all service APIs being produced.
|
||||
*/
|
||||
export class PluginServices<Services extends Record<keyof Services, {}>> {
|
||||
private registry: PluginServiceRegistry<Services, any> | null = null;
|
||||
|
||||
/**
|
||||
* Supply a `PluginServiceRegistry` for the class to use to provide services and context.
|
||||
*
|
||||
* @param registry A setup and started `PluginServiceRegistry`.
|
||||
*/
|
||||
setRegistry(registry: PluginServiceRegistry<Services, any> | null) {
|
||||
if (registry && !registry.isStarted()) {
|
||||
throw new Error('Registry has not been started.');
|
||||
}
|
||||
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a registry has been provided, false otherwise.
|
||||
*/
|
||||
hasRegistry() {
|
||||
return !!this.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private getter that will enforce proper setup throughout the class.
|
||||
*/
|
||||
private getRegistry() {
|
||||
if (!this.registry) {
|
||||
throw new Error('No registry has been provided.');
|
||||
}
|
||||
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the React Context Provider that will supply services.
|
||||
*/
|
||||
getContextProvider() {
|
||||
return this.getRegistry().getContextProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map of React Hooks that can be used in React components.
|
||||
*/
|
||||
getHooks(): ServiceHooks<Services> {
|
||||
const registry = this.getRegistry();
|
||||
const providers = registry.getServiceProviders();
|
||||
|
||||
const providerNames = Object.keys(providers) as Array<keyof typeof providers>;
|
||||
|
||||
return providerNames.reduce((acc, providerName) => {
|
||||
acc[providerName] = { useService: providers[providerName].getServiceReactHook() };
|
||||
return acc;
|
||||
}, {} as ServiceHooks<Services>);
|
||||
}
|
||||
|
||||
getServices(): Services {
|
||||
const registry = this.getRegistry();
|
||||
const providers = registry.getServiceProviders();
|
||||
|
||||
const providerNames = Object.keys(providers) as Array<keyof typeof providers>;
|
||||
|
||||
return providerNames.reduce((acc, providerName) => {
|
||||
acc[providerName] = providers[providerName].getService();
|
||||
return acc;
|
||||
}, {} as Services);
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, FC, PropsWithChildren } from 'react';
|
||||
import { PluginServiceFactory } from './factory';
|
||||
|
||||
/**
|
||||
* A collection of `PluginServiceProvider` objects, keyed by the `Services` API generic.
|
||||
*
|
||||
* The `Services` generic determines the shape of all service APIs being produced.
|
||||
* The `StartParameters` generic determines what parameters are expected to
|
||||
* start the service.
|
||||
*/
|
||||
export type PluginServiceProviders<
|
||||
Services extends Record<keyof Services, {}>,
|
||||
StartParameters = {}
|
||||
> = {
|
||||
[K in keyof Services]: PluginServiceProvider<
|
||||
Services[K],
|
||||
StartParameters,
|
||||
Services,
|
||||
Array<keyof Services>
|
||||
>;
|
||||
};
|
||||
|
||||
type ElementOfArray<ArrayType extends readonly unknown[]> = ArrayType extends Array<
|
||||
infer ElementType
|
||||
>
|
||||
? ElementType
|
||||
: never;
|
||||
|
||||
export type PluginServiceRequiredServices<
|
||||
RequiredServices extends Array<keyof AvailableServices>,
|
||||
AvailableServices
|
||||
> = {
|
||||
[K in ElementOfArray<RequiredServices>]: AvailableServices[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* An object which uses a given factory to start, stop or provide a service.
|
||||
*
|
||||
* The `Service` generic determines the shape of the API being produced.
|
||||
* The `StartParameters` generic determines what parameters are expected to
|
||||
* start the service.
|
||||
*/
|
||||
export class PluginServiceProvider<
|
||||
Service extends {},
|
||||
StartParameters = {},
|
||||
Services = {},
|
||||
RequiredServices extends Array<keyof Services> = []
|
||||
> {
|
||||
private factory: PluginServiceFactory<
|
||||
Service,
|
||||
StartParameters,
|
||||
PluginServiceRequiredServices<RequiredServices, Services>
|
||||
>;
|
||||
private _requiredServices?: RequiredServices;
|
||||
private context = createContext<Service | null>(null);
|
||||
private pluginService: Service | null = null;
|
||||
public readonly Provider: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return <this.context.Provider value={this.getService()}>{children}</this.context.Provider>;
|
||||
};
|
||||
|
||||
constructor(
|
||||
factory: PluginServiceFactory<
|
||||
Service,
|
||||
StartParameters,
|
||||
PluginServiceRequiredServices<RequiredServices, Services>
|
||||
>,
|
||||
requiredServices?: RequiredServices
|
||||
) {
|
||||
this.factory = factory;
|
||||
this._requiredServices = requiredServices;
|
||||
this.context.displayName = 'PluginServiceContext';
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter that will enforce proper setup throughout the class.
|
||||
*/
|
||||
public getService() {
|
||||
if (!this.pluginService) {
|
||||
throw new Error('Service not started');
|
||||
}
|
||||
return this.pluginService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the service.
|
||||
*
|
||||
* @param params Parameters used to start the service.
|
||||
*/
|
||||
start(
|
||||
params: StartParameters,
|
||||
requiredServices: PluginServiceRequiredServices<RequiredServices, Services>
|
||||
) {
|
||||
this.pluginService = this.factory(params, requiredServices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function for providing a Context hook for the service.
|
||||
*/
|
||||
getServiceReactHook() {
|
||||
return () => {
|
||||
const service = useContext(this.context);
|
||||
|
||||
if (!service) {
|
||||
throw new Error('Provider is not set up correctly');
|
||||
}
|
||||
|
||||
return service;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the service.
|
||||
*/
|
||||
stop() {
|
||||
this.pluginService = null;
|
||||
}
|
||||
|
||||
public get requiredServices() {
|
||||
return this._requiredServices ?? [];
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { DependencyManager } from './dependency_manager';
|
||||
import { PluginServiceProviders, PluginServiceRequiredServices } from './provider';
|
||||
|
||||
export class PluginServiceProvidersMediator<
|
||||
Services extends Record<keyof Services, {}>,
|
||||
StartParameters
|
||||
> {
|
||||
constructor(private readonly providers: PluginServiceProviders<Services, StartParameters>) {}
|
||||
|
||||
start(params: StartParameters) {
|
||||
this.getOrderedDependencies().forEach((service) => {
|
||||
this.providers[service].start(params, this.getServiceDependencies(service));
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.getOrderedDependencies().forEach((service) => this.providers[service].stop());
|
||||
}
|
||||
|
||||
private getOrderedDependencies() {
|
||||
const dependenciesGraph = this.getGraphOfDependencies();
|
||||
return DependencyManager.orderDependencies<keyof Services>(dependenciesGraph);
|
||||
}
|
||||
|
||||
private getGraphOfDependencies() {
|
||||
return this.getProvidersNames().reduce<Record<keyof Services, Array<keyof Services>>>(
|
||||
(graph, vertex) => ({ ...graph, [vertex]: this.providers[vertex].requiredServices ?? [] }),
|
||||
{} as Record<keyof Services, Array<keyof Services>>
|
||||
);
|
||||
}
|
||||
|
||||
private getProvidersNames() {
|
||||
return Object.keys(this.providers) as Array<keyof Services>;
|
||||
}
|
||||
|
||||
private getServiceDependencies(service: keyof Services) {
|
||||
const requiredServices = this.providers[service].requiredServices ?? [];
|
||||
return this.getServicesByDeps(requiredServices);
|
||||
}
|
||||
|
||||
private getServicesByDeps(deps: Array<keyof Services>) {
|
||||
return deps.reduce<PluginServiceRequiredServices<Array<keyof Services>, Services>>(
|
||||
(services, dependency) => ({
|
||||
...services,
|
||||
[dependency]: this.providers[dependency].getService(),
|
||||
}),
|
||||
{} as PluginServiceRequiredServices<Array<keyof Services>, Services>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import { PluginServiceProvider, PluginServiceProviders } from './provider';
|
||||
import { PluginServiceProvidersMediator } from './providers_mediator';
|
||||
|
||||
/**
|
||||
* A `PluginServiceRegistry` maintains a set of service providers which can be collectively
|
||||
* started, stopped or retreived.
|
||||
*
|
||||
* The `Services` generic determines the shape of all service APIs being produced.
|
||||
* The `StartParameters` generic determines what parameters are expected to
|
||||
* start the service.
|
||||
*/
|
||||
export class PluginServiceRegistry<
|
||||
Services extends Record<keyof Services, {}>,
|
||||
StartParameters = {}
|
||||
> {
|
||||
private providers: PluginServiceProviders<Services, StartParameters>;
|
||||
private providersMediator: PluginServiceProvidersMediator<Services, StartParameters>;
|
||||
private _isStarted = false;
|
||||
|
||||
constructor(providers: PluginServiceProviders<Services, StartParameters>) {
|
||||
this.providers = providers;
|
||||
this.providersMediator = new PluginServiceProvidersMediator(providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the registry has been started, false otherwise.
|
||||
*/
|
||||
isStarted() {
|
||||
return this._isStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of `PluginServiceProvider` objects.
|
||||
*/
|
||||
getServiceProviders() {
|
||||
if (!this._isStarted) {
|
||||
throw new Error('Registry not started');
|
||||
}
|
||||
return this.providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React Context Provider for use in consuming applications.
|
||||
*/
|
||||
getContextProvider() {
|
||||
const values = Object.values(this.getServiceProviders()) as Array<
|
||||
PluginServiceProvider<any, any>
|
||||
>;
|
||||
|
||||
// Collect and combine Context.Provider elements from each Service Provider into a single
|
||||
// Functional Component.
|
||||
const provider: FC<PropsWithChildren<unknown>> = ({ children }) => (
|
||||
<>
|
||||
{values.reduceRight((acc, serviceProvider) => {
|
||||
return <serviceProvider.Provider>{acc}</serviceProvider.Provider>;
|
||||
}, children)}
|
||||
</>
|
||||
);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the registry.
|
||||
*
|
||||
* @param params Parameters used to start the registry.
|
||||
*/
|
||||
start(params: StartParameters) {
|
||||
this.providersMediator.start(params);
|
||||
this._isStarted = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the registry.
|
||||
*/
|
||||
stop() {
|
||||
this.providersMediator.stop();
|
||||
this._isStarted = false;
|
||||
return this;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue