Setting up and documenting Presentation Util (#88112)

This commit is contained in:
Clint Andrew Hall 2021-01-28 17:15:13 -06:00 committed by GitHub
parent 608efb0a3d
commit 55afba4a4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1360 additions and 249 deletions

View file

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

View file

@ -393,6 +393,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",

View file

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

View file

@ -1,3 +0,0 @@
# presentationUtil
Utilities and components used by the presentation-related plugins

View file

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

View file

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

View file

@ -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 {
@ -26,34 +24,43 @@ 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) => {
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 })));
}
setIsLoadingDashboards(false);
},
[savedObjectsClient]
);
// Initial dashboard load
useEffect(() => {
fetchDashboards('');
}, [fetchDashboards]);
// 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);
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);
};
fetchDashboards();
return () => {
cleanedUp = true;
};
}, [findDashboardsByTitle, query]);
return (
<EuiComboBox
@ -61,7 +68,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
defaultMessage: 'Search dashboards...',
})}
singleSelection={{ asPlainText: true }}
options={dashboards || []}
options={dashboardOptions || []}
selectedOptions={!!selectedDashboard ? [selectedDashboard] : undefined}
onChange={(e) => {
if (e.length) {
@ -72,7 +79,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
onChange(null);
}
}}
onSearchChange={fetchDashboards}
onSearchChange={setQuery}
isDisabled={isDisabled}
isLoading={isLoadingDashboards}
compressed={true}

View file

@ -9,18 +9,6 @@
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiRadio,
EuiIconTip,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
import { SavedObjectsClientContract } from '../../../../core/public';
import {
OnSaveProps,
@ -28,9 +16,9 @@ import {
SavedObjectSaveModal,
} from '../../../../plugins/saved_objects/public';
import { DashboardPicker } from './dashboard_picker';
import './saved_object_save_modal_dashboard.scss';
import { pluginServices } from '../services';
import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector';
interface SaveModalDocumentInfo {
id?: string;
@ -38,116 +26,50 @@ interface SaveModalDocumentInfo {
description?: string;
}
export interface DashboardSaveModalProps {
export interface SaveModalDashboardProps {
documentInfo: SaveModalDocumentInfo;
objectType: string;
onClose: () => void;
onSave: (props: OnSaveProps & { dashboardId: string | null }) => void;
savedObjectsClient: SavedObjectsClientContract;
tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
}
export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
const { documentInfo, savedObjectsClient, tagOptions } = props;
const initialCopyOnSave = !Boolean(documentInfo.id);
export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
const { documentInfo, tagOptions, objectType, onClose } = props;
const { id: documentId } = documentInfo;
const initialCopyOnSave = !Boolean(documentId);
const { capabilities } = pluginServices.getHooks();
const {
canAccessDashboards,
canCreateNewDashboards,
canEditDashboards,
} = capabilities.useService();
const disableDashboardOptions =
!canAccessDashboards() || (!canCreateNewDashboards && !canEditDashboards);
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>(
documentInfo.id ? null : 'existing'
documentId || disableDashboardOptions ? null : 'existing'
);
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(
null
);
const [copyOnSave, setCopyOnSave] = useState<boolean>(initialCopyOnSave);
const renderDashboardSelect = (state: SaveModalState) => {
const isDisabled = Boolean(!state.copyOnSave && documentInfo.id);
return (
<>
<EuiFormRow
label={
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<FormattedMessage
id="presentationUtil.saveModalDashboard.addToDashboardLabel"
defaultMessage="Add to dashboard"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
type="iInCircle"
content={
<FormattedMessage
id="presentationUtil.saveModalDashboard.dashboardInfoTooltip"
defaultMessage="Items added to a dashboard will not appear in the library and must be edited from the dashboard."
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
}
hasChildLabel={false}
>
<EuiPanel color="subdued" hasShadow={false} data-test-subj="add-to-dashboard-options">
<div>
<EuiRadio
checked={dashboardOption === 'existing'}
id="existing-dashboard-option"
name="dashboard-option"
label={i18n.translate(
'presentationUtil.saveModalDashboard.existingDashboardOptionLabel',
{
defaultMessage: 'Existing',
}
)}
onChange={() => setDashboardOption('existing')}
disabled={isDisabled}
/>
<div className="savAddDashboard__searchDashboards">
<DashboardPicker
savedObjectsClient={savedObjectsClient}
isDisabled={dashboardOption !== 'existing'}
onChange={(dash) => {
setSelectedDashboard(dash);
}}
/>
</div>
<EuiSpacer size="s" />
<EuiRadio
checked={dashboardOption === 'new'}
id="new-dashboard-option"
name="dashboard-option"
label={i18n.translate(
'presentationUtil.saveModalDashboard.newDashboardOptionLabel',
{
defaultMessage: 'New',
}
)}
onChange={() => setDashboardOption('new')}
disabled={isDisabled}
/>
<EuiSpacer size="s" />
<EuiRadio
checked={dashboardOption === null}
id="add-to-library-option"
name="dashboard-option"
label={i18n.translate('presentationUtil.saveModalDashboard.libraryOptionLabel', {
defaultMessage: 'No dashboard, but add to library',
})}
onChange={() => setDashboardOption(null)}
disabled={isDisabled}
/>
</div>
</EuiPanel>
</EuiFormRow>
</>
);
};
const rightOptions = !disableDashboardOptions
? () => (
<SaveModalDashboardSelector
onSelectDashboard={(dash) => {
setSelectedDashboard(dash);
}}
onChange={(option) => {
setDashboardOption(option);
}}
{...{ copyOnSave, documentId, dashboardOption }}
/>
)
: null;
const onCopyOnSaveChange = (newCopyOnSave: boolean) => {
setDashboardOption(null);
@ -159,7 +81,7 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
// Don't save with a dashboard ID if we're
// just updating an existing visualization
if (!(!onSaveProps.newCopyOnSave && documentInfo.id)) {
if (!(!onSaveProps.newCopyOnSave && documentId)) {
if (dashboardOption === 'existing') {
dashboardId = selectedDashboard?.id || null;
} else {
@ -171,13 +93,14 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
};
const saveLibraryLabel =
!copyOnSave && documentInfo.id
!copyOnSave && documentId
? i18n.translate('presentationUtil.saveModalDashboard.saveLabel', {
defaultMessage: 'Save',
})
: i18n.translate('presentationUtil.saveModalDashboard.saveToLibraryLabel', {
defaultMessage: 'Save and add to library',
});
const saveDashboardLabel = i18n.translate(
'presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel',
{
@ -192,18 +115,20 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
return (
<SavedObjectSaveModal
onSave={onModalSave}
onClose={props.onClose}
title={documentInfo.title}
showCopyOnSave={documentInfo.id ? true : false}
initialCopyOnSave={initialCopyOnSave}
confirmButtonLabel={confirmButtonLabel}
objectType={props.objectType}
showCopyOnSave={documentId ? true : false}
options={dashboardOption === null ? tagOptions : undefined} // Show tags when not adding to dashboard
rightOptions={renderDashboardSelect}
description={documentInfo.description}
showDescription={true}
isValid={isValid}
onCopyOnSaveChange={onCopyOnSaveChange}
{...{
confirmButtonLabel,
initialCopyOnSave,
isValid,
objectType,
onClose,
onCopyOnSaveChange,
rightOptions,
}}
/>
);
}

View file

@ -0,0 +1,57 @@
/*
* 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, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { StorybookParams } from '../services/storybook';
import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector';
export default {
component: SaveModalDashboardSelector,
title: 'Save Modal Dashboard Selector',
description: 'A selector for determining where an object will be saved after it is created.',
argTypes: {
hasDocumentId: {
control: 'boolean',
defaultValue: false,
},
copyOnSave: {
control: 'boolean',
defaultValue: false,
},
canCreateNewDashboards: {
control: 'boolean',
defaultValue: true,
},
canEditDashboards: {
control: 'boolean',
defaultValue: true,
},
},
};
export function Example({
copyOnSave,
hasDocumentId,
}: {
copyOnSave: boolean;
hasDocumentId: boolean;
} & StorybookParams) {
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing');
return (
<SaveModalDashboardSelector
onSelectDashboard={action('onSelect')}
onChange={setDashboardOption}
dashboardOption={dashboardOption}
copyOnSave={copyOnSave}
documentId={hasDocumentId ? 'abc' : undefined}
/>
);
}

View file

@ -0,0 +1,132 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiRadio,
EuiIconTip,
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
import { pluginServices } from '../services';
import { DashboardPicker, DashboardPickerProps } from './dashboard_picker';
import './saved_object_save_modal_dashboard.scss';
export interface SaveModalDashboardSelectorProps {
copyOnSave: boolean;
documentId?: string;
onSelectDashboard: DashboardPickerProps['onChange'];
dashboardOption: 'new' | 'existing' | null;
onChange: (dashboardOption: 'new' | 'existing' | null) => void;
}
export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) {
const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props;
const { capabilities } = pluginServices.getHooks();
const { canCreateNewDashboards, canEditDashboards } = capabilities.useService();
const isDisabled = !copyOnSave && !!documentId;
return (
<>
<EuiFormRow
label={
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<FormattedMessage
id="presentationUtil.saveModalDashboard.addToDashboardLabel"
defaultMessage="Add to dashboard"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
type="iInCircle"
content={
<FormattedMessage
id="presentationUtil.saveModalDashboard.dashboardInfoTooltip"
defaultMessage="Items added to a dashboard will not appear in the library and must be edited from the dashboard."
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
}
hasChildLabel={false}
>
<EuiPanel color="subdued" hasShadow={false} data-test-subj="add-to-dashboard-options">
<div>
{canEditDashboards() && (
<>
{' '}
<EuiRadio
checked={dashboardOption === 'existing'}
id="existing-dashboard-option"
name="dashboard-option"
label={i18n.translate(
'presentationUtil.saveModalDashboard.existingDashboardOptionLabel',
{
defaultMessage: 'Existing',
}
)}
onChange={() => onChange('existing')}
disabled={isDisabled}
/>
<div className="savAddDashboard__searchDashboards">
<DashboardPicker
isDisabled={dashboardOption !== 'existing'}
onChange={onSelectDashboard}
/>
</div>
<EuiSpacer size="s" />
</>
)}
{canCreateNewDashboards() && (
<>
{' '}
<EuiRadio
checked={dashboardOption === 'new'}
id="new-dashboard-option"
name="dashboard-option"
label={i18n.translate(
'presentationUtil.saveModalDashboard.newDashboardOptionLabel',
{
defaultMessage: 'New',
}
)}
onChange={() => onChange('new')}
disabled={isDisabled}
/>
<EuiSpacer size="s" />
</>
)}
<EuiRadio
checked={dashboardOption === null}
id="add-to-library-option"
name="dashboard-option"
label={i18n.translate('presentationUtil.saveModalDashboard.libraryOptionLabel', {
defaultMessage: 'No dashboard, but add to library',
})}
onChange={() => onChange(null)}
disabled={isDisabled}
/>
</div>
</EuiPanel>
</EuiFormRow>
</>
);
}

View file

@ -10,9 +10,11 @@ import { PresentationUtilPlugin } from './plugin';
export {
SavedObjectSaveModalDashboard,
DashboardSaveModalProps,
SaveModalDashboardProps,
} from './components/saved_object_save_modal_dashboard';
export { DashboardPicker } from './components/dashboard_picker';
export function plugin() {
return new PresentationUtilPlugin();
}

View file

@ -7,16 +7,39 @@
*/
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types';
import { pluginServices } from './services';
import { registry } from './services/kibana';
import {
PresentationUtilPluginSetup,
PresentationUtilPluginStart,
PresentationUtilPluginSetupDeps,
PresentationUtilPluginStartDeps,
} from './types';
export class PresentationUtilPlugin
implements Plugin<PresentationUtilPluginSetup, PresentationUtilPluginStart> {
public setup(core: CoreSetup): PresentationUtilPluginSetup {
implements
Plugin<
PresentationUtilPluginSetup,
PresentationUtilPluginStart,
PresentationUtilPluginSetupDeps,
PresentationUtilPluginStartDeps
> {
public setup(
_coreSetup: CoreSetup<PresentationUtilPluginSetup>,
_setupPlugins: PresentationUtilPluginSetupDeps
): PresentationUtilPluginSetup {
return {};
}
public start(core: CoreStart): PresentationUtilPluginStart {
return {};
public async start(
coreStart: CoreStart,
startPlugins: PresentationUtilPluginStartDeps
): Promise<PresentationUtilPluginStart> {
pluginServices.setRegistry(registry.start({ coreStart, startPlugins }));
return {
ContextProvider: pluginServices.getContextProvider(),
};
}
public stop() {}

View file

@ -0,0 +1,42 @@
/*
* 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 { BehaviorSubject } from 'rxjs';
import { CoreStart, AppUpdater } from 'src/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 = {}> = (params: Parameters) => 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>;
}
/**
* 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 {}> = (
params: KibanaPluginServiceParams<Start>
) => Service;

View file

@ -0,0 +1,82 @@
/*
* 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 { mapValues } from 'lodash';
import { PluginServiceRegistry } from './registry';
export { PluginServiceRegistry } from './registry';
export { PluginServiceProvider, PluginServiceProviders } from './provider';
export {
PluginServiceFactory,
KibanaPluginServiceFactory,
KibanaPluginServiceParams,
} from './factory';
/**
* `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> {
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(): { [K in keyof Services]: { useService: () => Services[K] } } {
const registry = this.getRegistry();
const providers = registry.getServiceProviders();
// @ts-expect-error Need to fix this; the type isn't fully understood when inferred.
return mapValues(providers, (provider) => ({
useService: provider.getUseServiceHook(),
}));
}
}

View file

@ -0,0 +1,83 @@
/*
* 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, { createContext, useContext } 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, StartParameters = {}> = {
[K in keyof Services]: PluginServiceProvider<Services[K], StartParameters>;
};
/**
* 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 = {}> {
private factory: PluginServiceFactory<Service, StartParameters>;
private context = createContext<Service | null>(null);
private pluginService: Service | null = null;
public readonly Provider: React.FC = ({ children }) => {
return <this.context.Provider value={this.getService()}>{children}</this.context.Provider>;
};
constructor(factory: PluginServiceFactory<Service, StartParameters>) {
this.factory = factory;
this.context.displayName = 'PluginServiceContext';
}
/**
* Private getter that will enforce proper setup throughout the class.
*/
private 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) {
this.pluginService = this.factory(params);
}
/**
* Returns a function for providing a Context hook for the service.
*/
getUseServiceHook() {
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;
}
}

View file

@ -0,0 +1,89 @@
/*
* 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 { values } from 'lodash';
import { PluginServiceProvider, PluginServiceProviders } from './provider';
/**
* 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, StartParameters = {}> {
private providers: PluginServiceProviders<Services, StartParameters>;
private _isStarted = false;
constructor(providers: PluginServiceProviders<Services, StartParameters>) {
this.providers = 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() {
// Collect and combine Context.Provider elements from each Service Provider into a single
// Functional Component.
const provider: React.FC = ({ children }) => (
<>
{values<PluginServiceProvider<any, any>>(this.getServiceProviders()).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) {
values<PluginServiceProvider<any, any>>(this.providers).map((serviceProvider) =>
serviceProvider.start(params)
);
this._isStarted = true;
return this;
}
/**
* Stop the registry.
*/
stop() {
values<PluginServiceProvider<any, any>>(this.providers).map((serviceProvider) =>
serviceProvider.stop()
);
this._isStarted = false;
return this;
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 { SimpleSavedObject } from 'src/core/public';
import { DashboardSavedObject } from 'src/plugins/dashboard/public';
import { PluginServices } from './create';
export interface PresentationDashboardsService {
findDashboards: (
query: string,
fields: string[]
) => Promise<Array<SimpleSavedObject<DashboardSavedObject>>>;
findDashboardsByTitle: (title: string) => Promise<Array<SimpleSavedObject<DashboardSavedObject>>>;
}
export interface PresentationCapabilitiesService {
canAccessDashboards: () => boolean;
canCreateNewDashboards: () => boolean;
canEditDashboards: () => boolean;
}
export interface PresentationUtilServices {
dashboards: PresentationDashboardsService;
capabilities: PresentationCapabilitiesService;
}
export const pluginServices = new PluginServices<PresentationUtilServices>();

View file

@ -0,0 +1,26 @@
/*
* 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 { PresentationUtilPluginStartDeps } from '../../types';
import { KibanaPluginServiceFactory } from '../create';
import { PresentationCapabilitiesService } from '..';
export type CapabilitiesServiceFactory = KibanaPluginServiceFactory<
PresentationCapabilitiesService,
PresentationUtilPluginStartDeps
>;
export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => {
const { dashboard } = coreStart.application.capabilities;
return {
canAccessDashboards: () => Boolean(dashboard.show),
canCreateNewDashboards: () => Boolean(dashboard.createNew),
canEditDashboards: () => !Boolean(dashboard.hideWriteControls),
};
};

View file

@ -0,0 +1,39 @@
/*
* 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 { DashboardSavedObject } from 'src/plugins/dashboard/public';
import { PresentationUtilPluginStartDeps } from '../../types';
import { KibanaPluginServiceFactory } from '../create';
import { PresentationDashboardsService } from '..';
export type DashboardsServiceFactory = KibanaPluginServiceFactory<
PresentationDashboardsService,
PresentationUtilPluginStartDeps
>;
export const dashboardsServiceFactory: DashboardsServiceFactory = ({ coreStart }) => {
const findDashboards = async (query: string = '', fields: string[] = []) => {
const { find } = coreStart.savedObjects.client;
const { savedObjects } = await find<DashboardSavedObject>({
type: 'dashboard',
search: `${query}*`,
searchFields: fields,
});
return savedObjects;
};
const findDashboardsByTitle = async (title: string = '') => findDashboards(title, ['title']);
return {
findDashboards,
findDashboardsByTitle,
};
};

View file

@ -0,0 +1,34 @@
/*
* 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 { dashboardsServiceFactory } from './dashboards';
import { capabilitiesServiceFactory } from './capabilities';
import {
PluginServiceProviders,
KibanaPluginServiceParams,
PluginServiceProvider,
PluginServiceRegistry,
} from '../create';
import { PresentationUtilPluginStartDeps } from '../../types';
import { PresentationUtilServices } from '..';
export { dashboardsServiceFactory } from './dashboards';
export { capabilitiesServiceFactory } from './capabilities';
export const providers: PluginServiceProviders<
PresentationUtilServices,
KibanaPluginServiceParams<PresentationUtilPluginStartDeps>
> = {
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
};
export const registry = new PluginServiceRegistry<
PresentationUtilServices,
KibanaPluginServiceParams<PresentationUtilPluginStartDeps>
>(providers);

View file

@ -0,0 +1,29 @@
/*
* 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 { PluginServiceFactory } from '../create';
import { StorybookParams } from '.';
import { PresentationCapabilitiesService } from '..';
type CapabilitiesServiceFactory = PluginServiceFactory<
PresentationCapabilitiesService,
StorybookParams
>;
export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({
canAccessDashboards,
canCreateNewDashboards,
canEditDashboards,
}) => {
const check = (value: boolean = true) => value;
return {
canAccessDashboards: () => check(canAccessDashboards),
canCreateNewDashboards: () => check(canCreateNewDashboards),
canEditDashboards: () => check(canEditDashboards),
};
};

View file

@ -0,0 +1,28 @@
/*
* 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 { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create';
import { dashboardsServiceFactory } from '../stub/dashboards';
import { capabilitiesServiceFactory } from './capabilities';
import { PresentationUtilServices } from '..';
export { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
export { PresentationUtilServices } from '..';
export interface StorybookParams {
canAccessDashboards?: boolean;
canCreateNewDashboards?: boolean;
canEditDashboards?: boolean;
}
export const providers: PluginServiceProviders<PresentationUtilServices, StorybookParams> = {
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
};
export const pluginServices = new PluginServices<PresentationUtilServices>();

View file

@ -0,0 +1,18 @@
/*
* 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 { PluginServiceFactory } from '../create';
import { PresentationCapabilitiesService } from '..';
type CapabilitiesServiceFactory = PluginServiceFactory<PresentationCapabilitiesService>;
export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({
canAccessDashboards: () => true,
canCreateNewDashboards: () => true,
canEditDashboards: () => true,
});

View file

@ -0,0 +1,37 @@
/*
* 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 { PluginServiceFactory } from '../create';
import { PresentationDashboardsService } from '..';
// TODO (clint): Create set of dashboards to stub and return.
type DashboardsServiceFactory = PluginServiceFactory<PresentationDashboardsService>;
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const dashboardsServiceFactory: DashboardsServiceFactory = () => ({
findDashboards: async (query: string = '', _fields: string[] = []) => {
if (!query) {
return [];
}
await sleep(2000);
return [];
},
findDashboardsByTitle: async (title: string) => {
if (!title) {
return [];
}
await sleep(2000);
return [];
},
});

View file

@ -0,0 +1,22 @@
/*
* 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 { dashboardsServiceFactory } from './dashboards';
import { capabilitiesServiceFactory } from './capabilities';
import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create';
import { PresentationUtilServices } from '..';
export { dashboardsServiceFactory } from './dashboards';
export { capabilitiesServiceFactory } from './capabilities';
export const providers: PluginServiceProviders<PresentationUtilServices> = {
dashboards: new PluginServiceProvider(dashboardsServiceFactory),
capabilities: new PluginServiceProvider(capabilitiesServiceFactory),
};
export const registry = new PluginServiceRegistry<PresentationUtilServices>(providers);

View file

@ -8,5 +8,12 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PresentationUtilPluginSetup {}
export interface PresentationUtilPluginStart {
ContextProvider: React.FC;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PresentationUtilPluginStart {}
export interface PresentationUtilPluginSetupDeps {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PresentationUtilPluginStartDeps {}

View file

@ -0,0 +1,28 @@
/*
* 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 { DecoratorFn } from '@storybook/react';
import { I18nProvider } from '@kbn/i18n/react';
import { pluginServices } from '../public/services';
import { PresentationUtilServices } from '../public/services';
import { providers, StorybookParams } from '../public/services/storybook';
import { PluginServiceRegistry } from '../public/services/create';
export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => {
const registry = new PluginServiceRegistry<PresentationUtilServices, StorybookParams>(providers);
pluginServices.setRegistry(registry.start(storybook.args));
const ContextProvider = pluginServices.getContextProvider();
return (
<I18nProvider>
<ContextProvider>{story()}</ContextProvider>
</I18nProvider>
);
};

View file

@ -0,0 +1,19 @@
/*
* 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 { Configuration } from 'webpack';
import { defaultConfig } from '@kbn/storybook';
import webpackConfig from '@kbn/storybook/target/webpack.config';
module.exports = {
...defaultConfig,
addons: ['@storybook/addon-essentials'],
webpackFinal: (config: Configuration) => {
return webpackConfig({ config });
},
};

View file

@ -0,0 +1,21 @@
/*
* 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 { addons } from '@storybook/addons';
import { create } from '@storybook/theming';
import { PANEL_ID } from '@storybook/addon-actions';
addons.setConfig({
theme: create({
base: 'light',
brandTitle: 'Kibana Presentation Utility Storybook',
brandUrl: 'https://github.com/elastic/kibana/tree/master/src/plugins/presentation_util',
}),
showPanel: true.valueOf,
selectedPanel: PANEL_ID,
});

View file

@ -0,0 +1,29 @@
/*
* 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 { addDecorator } from '@storybook/react';
import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks';
import { servicesContextDecorator } from './decorator';
addDecorator(servicesContextDecorator);
export const parameters = {
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Primary />
<Stories />
</>
),
},
};

View file

@ -7,7 +7,7 @@
"declaration": true,
"declarationMap": true
},
"include": ["common/**/*", "public/**/*"],
"include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../dashboard/tsconfig.json" },

View file

@ -31,7 +31,8 @@ interface MinimalSaveModalProps {
export function showSaveModal(
saveModal: React.ReactElement<MinimalSaveModalProps>,
I18nContext: I18nStart['Context']
I18nContext: I18nStart['Context'],
Wrapper?: React.FC
) {
const container = document.createElement('div');
const closeModal = () => {
@ -55,5 +56,13 @@ export function showSaveModal(
onClose: closeModal,
});
ReactDOM.render(<I18nContext>{element}</I18nContext>, container);
const wrappedElement = Wrapper ? (
<I18nContext>
<Wrapper>{element}</Wrapper>
</I18nContext>
) : (
<I18nContext>{element}</I18nContext>
);
ReactDOM.render(wrappedElement, container);
}

View file

@ -11,7 +11,8 @@
"visualizations",
"embeddable",
"dashboard",
"uiActions"
"uiActions",
"presentationUtil"
],
"optionalPlugins": [
"home",
@ -22,7 +23,6 @@
"kibanaUtils",
"kibanaReact",
"home",
"presentationUtil",
"discover"
]
}

View file

@ -69,7 +69,6 @@ const TopNav = ({
},
[visInstance.embeddableHandler]
);
const savedObjectsClient = services.savedObjects.client;
const config = useMemo(() => {
if (isEmbeddableRendered) {
@ -85,7 +84,6 @@ const TopNav = ({
stateContainer,
visualizationIdFromUrl,
stateTransfer: services.stateTransferService,
savedObjectsClient,
embeddableId,
},
services
@ -104,7 +102,6 @@ const TopNav = ({
visualizationIdFromUrl,
services,
embeddableId,
savedObjectsClient,
]);
const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>(
vis.data.indexPattern ? [vis.data.indexPattern] : []

View file

@ -30,9 +30,11 @@ export const renderApp = (
const app = (
<Router history={services.history}>
<KibanaContextProvider services={services}>
<services.i18n.Context>
<VisualizeApp onAppLeave={onAppLeave} />
</services.i18n.Context>
<services.presentationUtil.ContextProvider>
<services.i18n.Context>
<VisualizeApp onAppLeave={onAppLeave} />
</services.i18n.Context>
</services.presentationUtil.ContextProvider>
</KibanaContextProvider>
</Router>
);

View file

@ -34,6 +34,7 @@ import { SharePluginStart } from 'src/plugins/share/public';
import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public';
import { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
import { UrlForwardingStart } from 'src/plugins/url_forwarding/public';
import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public';
import { EventEmitter } from 'events';
import { DashboardStart } from '../../../dashboard/public';
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
@ -93,6 +94,7 @@ export interface VisualizeServices extends CoreStart {
dashboard: DashboardStart;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
savedObjectsTagging?: SavedObjectsTaggingApi;
presentationUtil: PresentationUtilPluginStart;
}
export interface SavedVisInstance {

View file

@ -20,7 +20,6 @@ import {
} from '../../../../saved_objects/public';
import { SavedObjectSaveModalDashboard } from '../../../../presentation_util/public';
import { unhashUrl } from '../../../../kibana_utils/public';
import { SavedObjectsClientContract } from '../../../../../core/public';
import {
VisualizeServices,
@ -50,7 +49,6 @@ interface TopNavConfigParams {
stateContainer: VisualizeAppStateContainer;
visualizationIdFromUrl?: string;
stateTransfer: EmbeddableStateTransfer;
savedObjectsClient: SavedObjectsClientContract;
embeddableId?: string;
}
@ -72,7 +70,6 @@ export const getTopNavConfig = (
hasUnappliedChanges,
visInstance,
stateContainer,
savedObjectsClient,
visualizationIdFromUrl,
stateTransfer,
embeddableId,
@ -88,6 +85,7 @@ export const getTopNavConfig = (
i18n: { Context: I18nContext },
dashboard,
savedObjectsTagging,
presentationUtil,
}: VisualizeServices
) => {
const { vis, embeddableHandler } = visInstance;
@ -397,39 +395,43 @@ export const getTopNavConfig = (
);
}
const saveModal =
!!originatingApp ||
!dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables ? (
<SavedObjectSaveModalOrigin
documentInfo={savedVis || { title: '' }}
onSave={onSave}
options={tagOptions}
getAppNameFromId={stateTransfer.getAppNameFromId}
objectType={'visualization'}
onClose={() => {}}
originatingApp={originatingApp}
returnToOriginSwitchLabel={
originatingApp && embeddableId
? i18n.translate('visualize.topNavMenu.updatePanel', {
defaultMessage: 'Update panel on {originatingAppName}',
values: {
originatingAppName: stateTransfer.getAppNameFromId(originatingApp),
},
})
: undefined
}
/>
) : (
<SavedObjectSaveModalDashboard
documentInfo={savedVis || { title: '' }}
onSave={onSave}
tagOptions={tagOptions}
objectType={'visualization'}
onClose={() => {}}
savedObjectsClient={savedObjectsClient}
/>
);
showSaveModal(saveModal, I18nContext);
const useByRefFlow =
!!originatingApp || !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables;
const saveModal = useByRefFlow ? (
<SavedObjectSaveModalOrigin
documentInfo={savedVis || { title: '' }}
onSave={onSave}
options={tagOptions}
getAppNameFromId={stateTransfer.getAppNameFromId}
objectType={'visualization'}
onClose={() => {}}
originatingApp={originatingApp}
returnToOriginSwitchLabel={
originatingApp && embeddableId
? i18n.translate('visualize.topNavMenu.updatePanel', {
defaultMessage: 'Update panel on {originatingAppName}',
values: {
originatingAppName: stateTransfer.getAppNameFromId(originatingApp),
},
})
: undefined
}
/>
) : (
<SavedObjectSaveModalDashboard
documentInfo={savedVis || { title: '' }}
onSave={onSave}
tagOptions={tagOptions}
objectType={'visualization'}
onClose={() => {}}
/>
);
showSaveModal(
saveModal,
I18nContext,
!useByRefFlow ? presentationUtil.ContextProvider : React.Fragment
);
},
},
]

View file

@ -20,6 +20,7 @@ import {
ScopedHistory,
} from 'kibana/public';
import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
import {
Storage,
createKbnUrlTracker,
@ -62,6 +63,7 @@ export interface VisualizePluginStartDependencies {
savedObjects: SavedObjectsStart;
dashboard: DashboardStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
presentationUtil: PresentationUtilPluginStart;
}
export interface VisualizePluginSetupDependencies {
@ -204,6 +206,7 @@ export class VisualizePlugin
dashboard: pluginsStart.dashboard,
setHeaderActionMenu: params.setHeaderActionMenu,
savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(),
presentationUtil: pluginsStart.presentationUtil,
};
params.element.classList.add('visAppWrapper');

7
typings/index.d.ts vendored
View file

@ -23,3 +23,10 @@ declare module '*.svg' {
// eslint-disable-next-line import/no-default-export
export default content;
}
// Storybook references this module. It's @ts-ignored in the codebase but when
// built into its dist it strips that out. Add it here to avoid a type checking
// error.
//
// See https://github.com/storybookjs/storybook/issues/11684
declare module 'react-syntax-highlighter/dist/cjs/create-element';

View file

@ -14,10 +14,26 @@
"dashboard",
"uiActions",
"embeddable",
"share"
"share",
"presentationUtil"
],
"optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"],
"configPath": ["xpack", "lens"],
"extraPublicDirs": ["common/constants"],
"requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "presentationUtil"]
"optionalPlugins": [
"usageCollection",
"taskManager",
"globalSearch",
"savedObjectsTagging"
],
"configPath": [
"xpack",
"lens"
],
"extraPublicDirs": [
"common/constants"
],
"requiredBundles": [
"savedObjects",
"kibanaUtils",
"kibanaReact",
"embeddable"
]
}

View file

@ -707,7 +707,6 @@ export function App({
isVisible={state.isSaveModalVisible}
originatingApp={state.isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined}
allowByValueEmbeddables={dashboardFeatureFlag.allowByValueEmbeddables}
savedObjectsClient={savedObjectsClient}
savedObjectsTagging={savedObjectsTagging}
tagsIds={tagsIds}
onSave={runSave}

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback } from 'react';
import React, { FC, useCallback } from 'react';
import { AppMountParameters, CoreSetup } from 'kibana/public';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
@ -39,9 +39,15 @@ export async function mountApp(
createEditorFrame: EditorFrameStart['createInstance'];
getByValueFeatureFlag: () => Promise<DashboardFeatureFlagConfig>;
attributeService: () => Promise<LensAttributeService>;
getPresentationUtilContext: () => Promise<FC>;
}
) {
const { createEditorFrame, getByValueFeatureFlag, attributeService } = mountProps;
const {
createEditorFrame,
getByValueFeatureFlag,
attributeService,
getPresentationUtilContext,
} = mountProps;
const [coreStart, startDependencies] = await core.getStartServices();
const { data, navigation, embeddable, savedObjectsTagging } = startDependencies;
@ -196,21 +202,26 @@ export async function mountApp(
});
params.element.classList.add('lnsAppWrapper');
const PresentationUtilContext = await getPresentationUtilContext();
render(
<I18nProvider>
<KibanaContextProvider services={lensServices}>
<HashRouter>
<Switch>
<Route exact path="/edit/:id" component={EditorRoute} />
<Route
exact
path={`/${LENS_EDIT_BY_VALUE}`}
render={(routeProps) => <EditorRoute {...routeProps} editByValue />}
/>
<Route exact path="/" component={EditorRoute} />
<Route path="/" component={NotFound} />
</Switch>
</HashRouter>
<PresentationUtilContext>
<HashRouter>
<Switch>
<Route exact path="/edit/:id" component={EditorRoute} />
<Route
exact
path={`/${LENS_EDIT_BY_VALUE}`}
render={(routeProps) => <EditorRoute {...routeProps} editByValue />}
/>
<Route exact path="/" component={EditorRoute} />
<Route path="/" component={NotFound} />
</Switch>
</HashRouter>
</PresentationUtilContext>
</KibanaContextProvider>
</I18nProvider>,
params.element

View file

@ -7,8 +7,6 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { SavedObjectsStart } from '../../../../../src/core/public';
import { Document } from '../persistence';
import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
@ -29,8 +27,6 @@ export interface Props {
originatingApp?: string;
allowByValueEmbeddables: boolean;
savedObjectsClient: SavedObjectsStart['client'];
savedObjectsTagging?: SavedObjectTaggingPluginStart;
tagsIds: string[];
@ -51,7 +47,6 @@ export const SaveModal = (props: Props) => {
const {
originatingApp,
savedObjectsTagging,
savedObjectsClient,
tagsIds,
lastKnownDoc,
allowByValueEmbeddables,
@ -88,7 +83,6 @@ export const SaveModal = (props: Props) => {
return (
<TagEnhancedSavedObjectSaveModalDashboard
savedObjectsTagging={savedObjectsTagging}
savedObjectsClient={savedObjectsClient}
initialTags={tagsIds}
onSave={(saveProps) => {
const saveToLibrary = saveProps.dashboardId === null;

View file

@ -7,7 +7,7 @@
import React, { FC, useState, useMemo, useCallback } from 'react';
import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public';
import {
DashboardSaveModalProps,
SaveModalDashboardProps,
SavedObjectSaveModalDashboard,
} from '../../../../../src/plugins/presentation_util/public';
import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
@ -19,7 +19,7 @@ export type DashboardSaveProps = OnSaveProps & {
};
export type TagEnhancedSavedObjectSaveModalDashboardProps = Omit<
DashboardSaveModalProps,
SaveModalDashboardProps,
'onSave'
> & {
initialTags: string[];
@ -48,7 +48,7 @@ export const TagEnhancedSavedObjectSaveModalDashboard: FC<TagEnhancedSavedObject
const tagEnhancedOptions = <>{tagSelectorOption}</>;
const tagEnhancedOnSave: DashboardSaveModalProps['onSave'] = useCallback(
const tagEnhancedOnSave: SaveModalDashboardProps['onSave'] = useCallback(
(saveOptions) => {
onSave({
...saveOptions,

View file

@ -17,6 +17,7 @@ import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/
import { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public';
import { GlobalSearchPluginSetup } from '../../global_search/public';
import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public';
import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public';
import { EditorFrameService } from './editor_frame_service';
import {
@ -71,6 +72,7 @@ export interface LensPluginStartDependencies {
embeddable: EmbeddableStart;
charts: ChartsPluginStart;
savedObjectsTagging?: SavedObjectTaggingPluginStart;
presentationUtil: PresentationUtilPluginStart;
}
export interface LensPublicStart {
@ -172,6 +174,12 @@ export class LensPlugin {
return deps.dashboard.dashboardFeatureFlagConfig;
};
const getPresentationUtilContext = async () => {
const [, deps] = await core.getStartServices();
const { ContextProvider } = deps.presentationUtil;
return ContextProvider;
};
core.application.register({
id: 'lens',
title: NOT_INTERNATIONALIZED_PRODUCT_NAME,
@ -183,6 +191,7 @@ export class LensPlugin {
createEditorFrame: this.createEditorFrame!,
attributeService: this.attributeService!,
getByValueFeatureFlag,
getPresentationUtilContext,
});
},
});

View file

@ -2,7 +2,10 @@
"id": "maps",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "maps"],
"configPath": [
"xpack",
"maps"
],
"requiredPlugins": [
"licensing",
"features",
@ -17,11 +20,21 @@
"mapsLegacy",
"usageCollection",
"savedObjects",
"share"
"share",
"presentationUtil"
],
"optionalPlugins": [
"home",
"savedObjectsTagging"
],
"optionalPlugins": ["home", "savedObjectsTagging"],
"ui": true,
"server": true,
"extraPublicDirs": ["common/constants"],
"requiredBundles": ["kibanaReact", "kibanaUtils", "home", "presentationUtil"]
"extraPublicDirs": [
"common/constants"
],
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"home"
]
}

View file

@ -49,6 +49,7 @@ export const getSearchService = () => pluginsStart.data.search;
export const getEmbeddableService = () => pluginsStart.embeddable;
export const getNavigateToApp = () => coreStart.application.navigateToApp;
export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging;
export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider;
// xpack.maps.* kibana.yml settings from this plugin
let mapAppConfig: MapsConfigType;

View file

@ -55,6 +55,7 @@ import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public';
import { StartContract as FileUploadStartContract } from '../../maps_file_upload/public';
import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public';
import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
import {
getIsEnterprisePlus,
registerLicensedFeatures,
@ -86,6 +87,7 @@ export interface MapsPluginStartDependencies {
savedObjects: SavedObjectsStart;
dashboard: DashboardStart;
savedObjectsTagging?: SavedObjectTaggingPluginStart;
presentationUtil: PresentationUtilPluginStart;
}
/**

View file

@ -16,6 +16,7 @@ import {
getSavedObjectsClient,
getCoreOverlays,
getSavedObjectsTagging,
getPresentationUtilContext,
} from '../../kibana_services';
import {
checkForDuplicateTitle,
@ -185,7 +186,7 @@ export function getTopNavConfig({
defaultMessage: 'map',
}),
};
const PresentationUtilContext = getPresentationUtilContext();
const saveModal =
savedMap.getOriginatingApp() || !getIsAllowByValueEmbeddables() ? (
<SavedObjectSaveModalOrigin
@ -195,14 +196,10 @@ export function getTopNavConfig({
options={tagSelector}
/>
) : (
<SavedObjectSaveModalDashboard
{...saveModalProps}
savedObjectsClient={getSavedObjectsClient()}
tagOptions={tagSelector}
/>
<SavedObjectSaveModalDashboard {...saveModalProps} tagOptions={tagSelector} />
);
showSaveModal(saveModal, getCoreI18n().Context);
showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext);
},
});

View file

@ -4445,7 +4445,7 @@
core-js "^3.0.1"
ts-dedent "^1.1.1"
"@storybook/addon-docs@6.0.26":
"@storybook/addon-docs@6.0.26", "@storybook/addon-docs@^6.0.26":
version "6.0.26"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.26.tgz#bd7fc1fcdc47bb7992fa8d3254367e8c3bba373d"
integrity sha512-3t8AOPkp8ZW74h7FnzxF3wAeb1wRyYjMmgJZxqzgi/x7K0i1inbCq8MuJnytuTcZ7+EK4HR6Ih7o9tJuAtIBLw==