id | slug | title | summary | date | tags | related | |||
---|---|---|---|---|---|---|---|---|---|
presentationUtilPlugin |
/kibana-dev-docs/presentationPlugin |
Presentation Utility Plugin |
Introduction to the Presentation Utility Plugin. |
2020-01-12 |
|
The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).
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.
- A
PluginServiceFactory
is a function that will return a set of functions-- which comprise aService
-- given a set of parameters. - A
PluginServiceProvider
is an object that use a factory to start, stop or provide aService
. - 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.
To start, a plugin should define a set of services it wants to provide to itself or other plugins.
```ts export interface PresentationDashboardsService { findDashboards: ( query: string, fields: string[] ) => Promise>>; findDashboardsByTitle: (title: string) => Promise>>; }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.
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)
export const serviceRegistry = new PluginServiceRegistry(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);
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.
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:
export interface PresentationFooService {
getFoo: () => string;
setFoo: (bar: string) => void;
}
a factory for a stubbed version might look like this:
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:
export type FooServiceFactory = KibanaPluginServiceFactory<
PresentationFooService,
PresentationUtilPluginStart
>;
export const fooServiceFactory: FooServiceFactory = ({
coreStart,
startPlugins,
}) => {
// ...do something with Kibana services...
return {
getFoo: //...
setFoo: //...
}
}
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:
public start( coreStart: CoreStart, startPlugins: StartDeps ): Promise { 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>
)
and then, consume your services using provided hooks in a component:
```ts // component.tsimport { 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, embeddable input sync, and context creation. To use this system, a developer can wrap their components in the ReduxEmbeddableWrapper, supplying only an object 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>
```ts
// my_embeddable_reducers.ts
import { MyEmbeddableInput } from './my_embeddable';
export const myEmbeddableReducers = {
setSpecialBoolean: (
state: WritableDraft<MyEmbeddableInput>,
action: PayloadAction<MyEmbeddableInput['specialBoolean']>
) => {
state.specialBoolean = action.payload;
}
}
Because the ReduxEmbeddableWrapper is a lazy component, it also must be unwrapped with the withSuspense
component from Presentation Util. When you await this component, you must also pass in the type information so that the redux store and actions are properly typed.
import { withSuspense, LazyReduxEmbeddableWrapper, ReduxEmbeddableWrapperPropsWithChildren, } from '../../../../presentation_util/public';
export interface MyEmbeddableInput { specialBoolean: boolean }
const MyEmbeddableReduxWrapper = withSuspense< ReduxEmbeddableWrapperPropsWithChildren
(LazyReduxEmbeddableWrapper);
</DocAccordion>
The ReduxEmbeddableWrapper should be used inside of embeddable classes, and should wrap all components under the embeddable in the render function.
<DocAccordion buttonContent="Wrapping Embeddable Render" initialIsOpen>
```ts
// my_embeddable.tsx
public render(dom: HTMLElement) {
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
this.domNode = dom;
ReactDOM.render(
<MyEmbeddableReduxWrapper embeddable={this} reducers={myEmbeddableReducers}>
<MyEmbeddableComponent />
</MyEmbeddableReduxWrapper>,
dom
);
}
From components under the embeddable, actions, containerActions, and the current state of the redux store are accessed via the ReduxEmbeddableContext. This context requires the input type and the type of the reducers, and will return the appropriately typed actions, a hook for dispatching actions, a selector to get the current redux state, and a suite of containerActions
if the embeddable is a Container.
const { useEmbeddableSelector, useEmbeddableDispatch, actions: { setSpecialBoolean }, } = useReduxEmbeddableContext< MyEmbeddableInput, typeof myEmbeddableReducers
();
const dispatch = useEmbeddableDispatch();
// current state const { specialBoolean } = useEmbeddableSelector((state) => state);
// change specialBoolean after 5 seconds setTimeout(() => dispatch(setSpecialBoolean(false)), 5000);
</DocAccordion>