[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:
Hannah Mudge 2024-10-08 16:37:18 -06:00 committed by GitHub
parent 38d0bdd3de
commit 1053493c9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 6 additions and 836 deletions

View file

@ -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>
```

View file

@ -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';

View file

@ -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();
});
});
});

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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 ?? [];
}
}

View file

@ -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>
);
}
}

View file

@ -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;
}
}