Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting up Presentation Util; Create Service Abstraction API #88112

Merged
merged 4 commits into from
Jan 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ It also provides a stateful version of it on the start contract.
Content is fetched from the remote (https://feeds.elastic.co and https://feeds-staging.elastic.co in dev mode) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely.


|{kib-repo}blob/{branch}/src/plugins/presentation_util/README.md[presentationUtil]
|Utilities and components used by the presentation-related plugins
|{kib-repo}blob/{branch}/src/plugins/presentation_util/README.mdx[presentationUtil]
|The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).


|{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@
"@storybook/addon-essentials": "^6.0.26",
"@storybook/addon-knobs": "^6.0.26",
"@storybook/addon-storyshots": "^6.0.26",
"@storybook/addon-docs": "^6.0.26",
"@storybook/components": "^6.0.26",
"@storybook/core": "^6.0.26",
"@storybook/core-events": "^6.0.26",
Expand Down
1 change: 1 addition & 0 deletions src/dev/storybook/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export const storybookAliases = {
security_solution: 'x-pack/plugins/security_solution/.storybook',
ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook',
observability: 'x-pack/plugins/observability/.storybook',
presentation: 'src/plugins/presentation_util/storybook',
};
3 changes: 0 additions & 3 deletions src/plugins/presentation_util/README.md

This file was deleted.

211 changes: 211 additions & 0 deletions src/plugins/presentation_util/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
---
id: presentationUtilPlugin
slug: /kibana-dev-docs/presentationPlugin
title: Presentation Utility Plugin
summary: Introduction to the Presentation Utility Plugin.
date: 2020-01-12
tags: ['kibana', 'presentation', 'services']
related: []
---

## Introduction

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 async 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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/

import React from 'react';
import { action } from '@storybook/addon-actions';

import { DashboardPicker } from './dashboard_picker';

export default {
component: DashboardPicker,
title: 'Dashboard Picker',
argTypes: {
isDisabled: {
control: 'boolean',
defaultValue: false,
},
},
};

export const Example = ({ isDisabled }: { isDisabled: boolean }) => (
<DashboardPicker onChange={action('onChange')} isDisabled={isDisabled} />
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
* Public License, v 1.
*/

import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect } from 'react';

import { i18n } from '@kbn/i18n';

import { EuiComboBox } from '@elastic/eui';
import { SavedObjectsClientContract } from '../../../../core/public';
import { DashboardSavedObject } from '../../../../plugins/dashboard/public';
import { pluginServices } from '../services';

export interface DashboardPickerProps {
onChange: (dashboard: { name: string; id: string } | null) => void;
isDisabled: boolean;
savedObjectsClient: SavedObjectsClientContract;
}

interface DashboardOption {
Expand All @@ -26,42 +24,51 @@ interface DashboardOption {
}

export function DashboardPicker(props: DashboardPickerProps) {
const [dashboards, setDashboards] = useState<DashboardOption[]>([]);
const [dashboardOptions, setDashboardOptions] = useState<DashboardOption[]>([]);
const [isLoadingDashboards, setIsLoadingDashboards] = useState(true);
const [selectedDashboard, setSelectedDashboard] = useState<DashboardOption | null>(null);
const [query, setQuery] = useState('');

const { savedObjectsClient, isDisabled, onChange } = props;
const { isDisabled, onChange } = props;
const { dashboards } = pluginServices.getHooks();
const { findDashboardsByTitle } = dashboards.useService();

const fetchDashboards = useCallback(
async (query) => {
useEffect(() => {
// We don't want to manipulate the React state if the component has been unmounted
// while we wait for the saved objects to return.
let cleanedUp = false;

const fetchDashboards = async () => {
setIsLoadingDashboards(true);
setDashboards([]);

const { savedObjects } = await savedObjectsClient.find<DashboardSavedObject>({
type: 'dashboard',
search: query ? `${query}*` : '',
searchFields: ['title'],
});
if (savedObjects) {
setDashboards(savedObjects.map((d) => ({ value: d.id, label: d.attributes.title })));
setDashboardOptions([]);

const objects = await findDashboardsByTitle(query ? `${query}*` : '');

if (cleanedUp) {
return;
}

if (objects) {
setDashboardOptions(objects.map((d) => ({ value: d.id, label: d.attributes.title })));
}

setIsLoadingDashboards(false);
},
[savedObjectsClient]
);
};

// Initial dashboard load
useEffect(() => {
fetchDashboards('');
}, [fetchDashboards]);
fetchDashboards();

return () => {
cleanedUp = true;
};
}, [findDashboardsByTitle, query]);

return (
<EuiComboBox
placeholder={i18n.translate('presentationUtil.dashboardPicker.searchDashboardPlaceholder', {
defaultMessage: 'Search dashboards...',
})}
singleSelection={{ asPlainText: true }}
options={dashboards || []}
options={dashboardOptions || []}
selectedOptions={!!selectedDashboard ? [selectedDashboard] : undefined}
onChange={(e) => {
if (e.length) {
Expand All @@ -72,7 +79,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
onChange(null);
}
}}
onSearchChange={fetchDashboards}
onSearchChange={setQuery}
isDisabled={isDisabled}
isLoading={isLoadingDashboards}
compressed={true}
Expand Down
Loading