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. 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] |{kib-repo}blob/{branch}/src/plugins/presentation_util/README.mdx[presentationUtil]
|Utilities and components used by the presentation-related plugins |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] |{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-essentials": "^6.0.26",
"@storybook/addon-knobs": "^6.0.26", "@storybook/addon-knobs": "^6.0.26",
"@storybook/addon-storyshots": "^6.0.26", "@storybook/addon-storyshots": "^6.0.26",
"@storybook/addon-docs": "^6.0.26",
"@storybook/components": "^6.0.26", "@storybook/components": "^6.0.26",
"@storybook/core": "^6.0.26", "@storybook/core": "^6.0.26",
"@storybook/core-events": "^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', security_solution: 'x-pack/plugins/security_solution/.storybook',
ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook',
observability: 'x-pack/plugins/observability/.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. * Public License, v 1.
*/ */
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiComboBox } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui';
import { SavedObjectsClientContract } from '../../../../core/public'; import { pluginServices } from '../services';
import { DashboardSavedObject } from '../../../../plugins/dashboard/public';
export interface DashboardPickerProps { export interface DashboardPickerProps {
onChange: (dashboard: { name: string; id: string } | null) => void; onChange: (dashboard: { name: string; id: string } | null) => void;
isDisabled: boolean; isDisabled: boolean;
savedObjectsClient: SavedObjectsClientContract;
} }
interface DashboardOption { interface DashboardOption {
@ -26,34 +24,43 @@ interface DashboardOption {
} }
export function DashboardPicker(props: DashboardPickerProps) { export function DashboardPicker(props: DashboardPickerProps) {
const [dashboards, setDashboards] = useState<DashboardOption[]>([]); const [dashboardOptions, setDashboardOptions] = useState<DashboardOption[]>([]);
const [isLoadingDashboards, setIsLoadingDashboards] = useState(true); const [isLoadingDashboards, setIsLoadingDashboards] = useState(true);
const [selectedDashboard, setSelectedDashboard] = useState<DashboardOption | null>(null); 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(() => { useEffect(() => {
fetchDashboards(''); // We don't want to manipulate the React state if the component has been unmounted
}, [fetchDashboards]); // 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 ( return (
<EuiComboBox <EuiComboBox
@ -61,7 +68,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
defaultMessage: 'Search dashboards...', defaultMessage: 'Search dashboards...',
})} })}
singleSelection={{ asPlainText: true }} singleSelection={{ asPlainText: true }}
options={dashboards || []} options={dashboardOptions || []}
selectedOptions={!!selectedDashboard ? [selectedDashboard] : undefined} selectedOptions={!!selectedDashboard ? [selectedDashboard] : undefined}
onChange={(e) => { onChange={(e) => {
if (e.length) { if (e.length) {
@ -72,7 +79,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
onChange(null); onChange(null);
} }
}} }}
onSearchChange={fetchDashboards} onSearchChange={setQuery}
isDisabled={isDisabled} isDisabled={isDisabled}
isLoading={isLoadingDashboards} isLoading={isLoadingDashboards}
compressed={true} compressed={true}

View file

@ -9,18 +9,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { i18n } from '@kbn/i18n'; 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 { import {
OnSaveProps, OnSaveProps,
@ -28,9 +16,9 @@ import {
SavedObjectSaveModal, SavedObjectSaveModal,
} from '../../../../plugins/saved_objects/public'; } from '../../../../plugins/saved_objects/public';
import { DashboardPicker } from './dashboard_picker';
import './saved_object_save_modal_dashboard.scss'; import './saved_object_save_modal_dashboard.scss';
import { pluginServices } from '../services';
import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector';
interface SaveModalDocumentInfo { interface SaveModalDocumentInfo {
id?: string; id?: string;
@ -38,116 +26,50 @@ interface SaveModalDocumentInfo {
description?: string; description?: string;
} }
export interface DashboardSaveModalProps { export interface SaveModalDashboardProps {
documentInfo: SaveModalDocumentInfo; documentInfo: SaveModalDocumentInfo;
objectType: string; objectType: string;
onClose: () => void; onClose: () => void;
onSave: (props: OnSaveProps & { dashboardId: string | null }) => void; onSave: (props: OnSaveProps & { dashboardId: string | null }) => void;
savedObjectsClient: SavedObjectsClientContract;
tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode); tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
} }
export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) { export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
const { documentInfo, savedObjectsClient, tagOptions } = props; const { documentInfo, tagOptions, objectType, onClose } = props;
const initialCopyOnSave = !Boolean(documentInfo.id); 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>( const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>(
documentInfo.id ? null : 'existing' documentId || disableDashboardOptions ? null : 'existing'
); );
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(
null null
); );
const [copyOnSave, setCopyOnSave] = useState<boolean>(initialCopyOnSave); const [copyOnSave, setCopyOnSave] = useState<boolean>(initialCopyOnSave);
const renderDashboardSelect = (state: SaveModalState) => { const rightOptions = !disableDashboardOptions
const isDisabled = Boolean(!state.copyOnSave && documentInfo.id); ? () => (
<SaveModalDashboardSelector
return ( onSelectDashboard={(dash) => {
<> setSelectedDashboard(dash);
<EuiFormRow }}
label={ onChange={(option) => {
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}> setDashboardOption(option);
<EuiFlexItem grow={false}> }}
<FormattedMessage {...{ copyOnSave, documentId, dashboardOption }}
id="presentationUtil.saveModalDashboard.addToDashboardLabel" />
defaultMessage="Add to dashboard" )
/> : null;
</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 onCopyOnSaveChange = (newCopyOnSave: boolean) => { const onCopyOnSaveChange = (newCopyOnSave: boolean) => {
setDashboardOption(null); setDashboardOption(null);
@ -159,7 +81,7 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
// Don't save with a dashboard ID if we're // Don't save with a dashboard ID if we're
// just updating an existing visualization // just updating an existing visualization
if (!(!onSaveProps.newCopyOnSave && documentInfo.id)) { if (!(!onSaveProps.newCopyOnSave && documentId)) {
if (dashboardOption === 'existing') { if (dashboardOption === 'existing') {
dashboardId = selectedDashboard?.id || null; dashboardId = selectedDashboard?.id || null;
} else { } else {
@ -171,13 +93,14 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
}; };
const saveLibraryLabel = const saveLibraryLabel =
!copyOnSave && documentInfo.id !copyOnSave && documentId
? i18n.translate('presentationUtil.saveModalDashboard.saveLabel', { ? i18n.translate('presentationUtil.saveModalDashboard.saveLabel', {
defaultMessage: 'Save', defaultMessage: 'Save',
}) })
: i18n.translate('presentationUtil.saveModalDashboard.saveToLibraryLabel', { : i18n.translate('presentationUtil.saveModalDashboard.saveToLibraryLabel', {
defaultMessage: 'Save and add to library', defaultMessage: 'Save and add to library',
}); });
const saveDashboardLabel = i18n.translate( const saveDashboardLabel = i18n.translate(
'presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel', 'presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel',
{ {
@ -192,18 +115,20 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
return ( return (
<SavedObjectSaveModal <SavedObjectSaveModal
onSave={onModalSave} onSave={onModalSave}
onClose={props.onClose}
title={documentInfo.title} title={documentInfo.title}
showCopyOnSave={documentInfo.id ? true : false} showCopyOnSave={documentId ? true : false}
initialCopyOnSave={initialCopyOnSave}
confirmButtonLabel={confirmButtonLabel}
objectType={props.objectType}
options={dashboardOption === null ? tagOptions : undefined} // Show tags when not adding to dashboard options={dashboardOption === null ? tagOptions : undefined} // Show tags when not adding to dashboard
rightOptions={renderDashboardSelect}
description={documentInfo.description} description={documentInfo.description}
showDescription={true} 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 { export {
SavedObjectSaveModalDashboard, SavedObjectSaveModalDashboard,
DashboardSaveModalProps, SaveModalDashboardProps,
} from './components/saved_object_save_modal_dashboard'; } from './components/saved_object_save_modal_dashboard';
export { DashboardPicker } from './components/dashboard_picker';
export function plugin() { export function plugin() {
return new PresentationUtilPlugin(); return new PresentationUtilPlugin();
} }

View file

@ -7,16 +7,39 @@
*/ */
import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; 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 export class PresentationUtilPlugin
implements Plugin<PresentationUtilPluginSetup, PresentationUtilPluginStart> { implements
public setup(core: CoreSetup): PresentationUtilPluginSetup { Plugin<
PresentationUtilPluginSetup,
PresentationUtilPluginStart,
PresentationUtilPluginSetupDeps,
PresentationUtilPluginStartDeps
> {
public setup(
_coreSetup: CoreSetup<PresentationUtilPluginSetup>,
_setupPlugins: PresentationUtilPluginSetupDeps
): PresentationUtilPluginSetup {
return {}; return {};
} }
public start(core: CoreStart): PresentationUtilPluginStart { public async start(
return {}; coreStart: CoreStart,
startPlugins: PresentationUtilPluginStartDeps
): Promise<PresentationUtilPluginStart> {
pluginServices.setRegistry(registry.start({ coreStart, startPlugins }));
return {
ContextProvider: pluginServices.getContextProvider(),
};
} }
public stop() {} 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 // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginSetup {}
export interface PresentationUtilPluginStart {
ContextProvider: React.FC;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface // 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, "declaration": true,
"declarationMap": true "declarationMap": true
}, },
"include": ["common/**/*", "public/**/*"], "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"],
"references": [ "references": [
{ "path": "../../core/tsconfig.json" }, { "path": "../../core/tsconfig.json" },
{ "path": "../dashboard/tsconfig.json" }, { "path": "../dashboard/tsconfig.json" },

View file

@ -31,7 +31,8 @@ interface MinimalSaveModalProps {
export function showSaveModal( export function showSaveModal(
saveModal: React.ReactElement<MinimalSaveModalProps>, saveModal: React.ReactElement<MinimalSaveModalProps>,
I18nContext: I18nStart['Context'] I18nContext: I18nStart['Context'],
Wrapper?: React.FC
) { ) {
const container = document.createElement('div'); const container = document.createElement('div');
const closeModal = () => { const closeModal = () => {
@ -55,5 +56,13 @@ export function showSaveModal(
onClose: closeModal, 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", "visualizations",
"embeddable", "embeddable",
"dashboard", "dashboard",
"uiActions" "uiActions",
"presentationUtil"
], ],
"optionalPlugins": [ "optionalPlugins": [
"home", "home",
@ -22,7 +23,6 @@
"kibanaUtils", "kibanaUtils",
"kibanaReact", "kibanaReact",
"home", "home",
"presentationUtil",
"discover" "discover"
] ]
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,7 @@ import {
ScopedHistory, ScopedHistory,
} from 'kibana/public'; } from 'kibana/public';
import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
import { import {
Storage, Storage,
createKbnUrlTracker, createKbnUrlTracker,
@ -62,6 +63,7 @@ export interface VisualizePluginStartDependencies {
savedObjects: SavedObjectsStart; savedObjects: SavedObjectsStart;
dashboard: DashboardStart; dashboard: DashboardStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
presentationUtil: PresentationUtilPluginStart;
} }
export interface VisualizePluginSetupDependencies { export interface VisualizePluginSetupDependencies {
@ -204,6 +206,7 @@ export class VisualizePlugin
dashboard: pluginsStart.dashboard, dashboard: pluginsStart.dashboard,
setHeaderActionMenu: params.setHeaderActionMenu, setHeaderActionMenu: params.setHeaderActionMenu,
savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(), savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(),
presentationUtil: pluginsStart.presentationUtil,
}; };
params.element.classList.add('visAppWrapper'); 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 // eslint-disable-next-line import/no-default-export
export default content; 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", "dashboard",
"uiActions", "uiActions",
"embeddable", "embeddable",
"share" "share",
"presentationUtil"
], ],
"optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"], "optionalPlugins": [
"configPath": ["xpack", "lens"], "usageCollection",
"extraPublicDirs": ["common/constants"], "taskManager",
"requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "presentationUtil"] "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} isVisible={state.isSaveModalVisible}
originatingApp={state.isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined} originatingApp={state.isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined}
allowByValueEmbeddables={dashboardFeatureFlag.allowByValueEmbeddables} allowByValueEmbeddables={dashboardFeatureFlag.allowByValueEmbeddables}
savedObjectsClient={savedObjectsClient}
savedObjectsTagging={savedObjectsTagging} savedObjectsTagging={savedObjectsTagging}
tagsIds={tagsIds} tagsIds={tagsIds}
onSave={runSave} onSave={runSave}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,10 @@
"id": "maps", "id": "maps",
"version": "8.0.0", "version": "8.0.0",
"kibanaVersion": "kibana", "kibanaVersion": "kibana",
"configPath": ["xpack", "maps"], "configPath": [
"xpack",
"maps"
],
"requiredPlugins": [ "requiredPlugins": [
"licensing", "licensing",
"features", "features",
@ -17,11 +20,21 @@
"mapsLegacy", "mapsLegacy",
"usageCollection", "usageCollection",
"savedObjects", "savedObjects",
"share" "share",
"presentationUtil"
],
"optionalPlugins": [
"home",
"savedObjectsTagging"
], ],
"optionalPlugins": ["home", "savedObjectsTagging"],
"ui": true, "ui": true,
"server": true, "server": true,
"extraPublicDirs": ["common/constants"], "extraPublicDirs": [
"requiredBundles": ["kibanaReact", "kibanaUtils", "home", "presentationUtil"] "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 getEmbeddableService = () => pluginsStart.embeddable;
export const getNavigateToApp = () => coreStart.application.navigateToApp; export const getNavigateToApp = () => coreStart.application.navigateToApp;
export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging;
export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider;
// xpack.maps.* kibana.yml settings from this plugin // xpack.maps.* kibana.yml settings from this plugin
let mapAppConfig: MapsConfigType; let mapAppConfig: MapsConfigType;

View file

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

View file

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

View file

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