Skip to content

Commit

Permalink
Setting up and documenting Presentation Util (#88112) (#89666)
Browse files Browse the repository at this point in the history
  • Loading branch information
clintandrewhall authored Jan 29, 2021
1 parent c3ad743 commit 7622da0
Show file tree
Hide file tree
Showing 50 changed files with 1,357 additions and 246 deletions.
4 changes: 2 additions & 2 deletions docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,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 @@ -390,6 +390,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

0 comments on commit 7622da0

Please sign in to comment.