mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[8.x] [embeddable] remove legacy embeddable factories from 'Add from library' flyout (#202823) (#203181)
# Backport This will backport the following commits from `main` to `8.x`: - [[embeddable] remove legacy embeddable factories from 'Add from library' flyout (#202823)](https://github.com/elastic/kibana/pull/202823) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Nathan Reese","email":"reese.nathan@elastic.co"},"sourceCommit":{"committedDate":"2024-12-05T19:55:40Z","message":"[embeddable] remove legacy embeddable factories from 'Add from library' flyout (#202823)\n\nPart of https://github.com/elastic/kibana/issues/180059\r\n\r\nPR removes legacy embeddable factory support from Canvas and Dashboard\r\n`Add from library` flyout\r\n\r\nPR also does the following clean-ups\r\n1) Renames folder, files, and component from `add_panel_flyout` to\r\n`add_from_library_flyout`. When component was originally created,\r\ndashboard `Add panel` button did not exist, and `Add from library`\r\nbutton was called `Add panel`. Now that dashboard contains `Add panel`\r\nand `Add from library` buttons, the old naming convention is super\r\nconfusing and not longer lines up with the current UI.\r\n2) moves registry to `add_from_library` folder so that the registry is\r\nin closer proximity to its usage.\r\n2) Renames `registerReactEmbeddableSavedObject` to\r\n`registerAddFromLibraryType` because\r\n`registerReactEmbeddableSavedObject` does not clearly specifying what\r\nthe registry enables.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"d508b5da9ce4664565c1594d23688663eff66522","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Embedding","Team:Presentation","release_note:skip","v9.0.0","ci:project-deploy-observability","project:embeddableRebuild","backport:version","v8.18.0"],"title":"[embeddable] remove legacy embeddable factories from 'Add from library' flyout","number":202823,"url":"https://github.com/elastic/kibana/pull/202823","mergeCommit":{"message":"[embeddable] remove legacy embeddable factories from 'Add from library' flyout (#202823)\n\nPart of https://github.com/elastic/kibana/issues/180059\r\n\r\nPR removes legacy embeddable factory support from Canvas and Dashboard\r\n`Add from library` flyout\r\n\r\nPR also does the following clean-ups\r\n1) Renames folder, files, and component from `add_panel_flyout` to\r\n`add_from_library_flyout`. When component was originally created,\r\ndashboard `Add panel` button did not exist, and `Add from library`\r\nbutton was called `Add panel`. Now that dashboard contains `Add panel`\r\nand `Add from library` buttons, the old naming convention is super\r\nconfusing and not longer lines up with the current UI.\r\n2) moves registry to `add_from_library` folder so that the registry is\r\nin closer proximity to its usage.\r\n2) Renames `registerReactEmbeddableSavedObject` to\r\n`registerAddFromLibraryType` because\r\n`registerReactEmbeddableSavedObject` does not clearly specifying what\r\nthe registry enables.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"d508b5da9ce4664565c1594d23688663eff66522"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202823","number":202823,"mergeCommit":{"message":"[embeddable] remove legacy embeddable factories from 'Add from library' flyout (#202823)\n\nPart of https://github.com/elastic/kibana/issues/180059\r\n\r\nPR removes legacy embeddable factory support from Canvas and Dashboard\r\n`Add from library` flyout\r\n\r\nPR also does the following clean-ups\r\n1) Renames folder, files, and component from `add_panel_flyout` to\r\n`add_from_library_flyout`. When component was originally created,\r\ndashboard `Add panel` button did not exist, and `Add from library`\r\nbutton was called `Add panel`. Now that dashboard contains `Add panel`\r\nand `Add from library` buttons, the old naming convention is super\r\nconfusing and not longer lines up with the current UI.\r\n2) moves registry to `add_from_library` folder so that the registry is\r\nin closer proximity to its usage.\r\n2) Renames `registerReactEmbeddableSavedObject` to\r\n`registerAddFromLibraryType` because\r\n`registerReactEmbeddableSavedObject` does not clearly specifying what\r\nthe registry enables.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"d508b5da9ce4664565c1594d23688663eff66522"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
This commit is contained in:
parent
f6d380993a
commit
8bce4806ac
25 changed files with 284 additions and 658 deletions
|
@ -81,13 +81,14 @@ export const RegisterEmbeddable = () => {
|
|||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiText>
|
||||
<h2>Saved object embeddables</h2>
|
||||
<h2>
|
||||
Show saved object type in <em>Add from library</em> menu
|
||||
</h2>
|
||||
<p>
|
||||
Embeddable factories, such as Lens, Maps, Links, that can reference saved objects should
|
||||
register their saved object types using{' '}
|
||||
<strong>registerReactEmbeddableSavedObject</strong>. The <em>Add from library</em> flyout
|
||||
on Dashboards uses this registry to list saved objects. The example function below could
|
||||
be called from the public start contract for a plugin.
|
||||
register their saved object types using <strong>registerAddFromLibraryType</strong>. The{' '}
|
||||
<em>Add from library</em> flyout on Dashboards uses this registry to list saved objects.
|
||||
The example function below could be called from the public start contract for a plugin.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
|
|
|
@ -14,14 +14,13 @@ const MY_SAVED_OBJECT_TYPE = 'mySavedObjectType';
|
|||
const APP_ICON = 'logoKibana';
|
||||
|
||||
export const registerMyEmbeddableSavedObject = (embeddableSetup: EmbeddableSetup) =>
|
||||
embeddableSetup.registerReactEmbeddableSavedObject({
|
||||
embeddableSetup.registerAddFromLibraryType({
|
||||
onAdd: (container, savedObject) => {
|
||||
container.addNewPanel({
|
||||
panelType: MY_EMBEDDABLE_TYPE,
|
||||
initialState: savedObject.attributes,
|
||||
});
|
||||
},
|
||||
embeddableType: MY_EMBEDDABLE_TYPE,
|
||||
savedObjectType: MY_SAVED_OBJECT_TYPE,
|
||||
savedObjectName: 'Some saved object',
|
||||
getIconForSavedObject: () => APP_ICON,
|
||||
|
|
|
@ -7,18 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { isErrorEmbeddable, openAddPanelFlyout } from '@kbn/embeddable-plugin/public';
|
||||
import { isErrorEmbeddable, openAddFromLibraryFlyout } from '@kbn/embeddable-plugin/public';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
|
||||
export function addFromLibrary(this: DashboardContainer) {
|
||||
if (isErrorEmbeddable(this)) return;
|
||||
this.openOverlay(
|
||||
openAddPanelFlyout({
|
||||
openAddFromLibraryFlyout({
|
||||
container: this,
|
||||
onAddPanel: (id: string) => {
|
||||
this.setScrollToPanelId(id);
|
||||
this.setHighlightPanelId(id);
|
||||
},
|
||||
onClose: () => {
|
||||
this.clearOverlays();
|
||||
},
|
||||
|
|
|
@ -414,7 +414,7 @@ export class DiscoverPlugin
|
|||
return this.getDiscoverServices(coreStart, deps, profilesManager, ebtManager);
|
||||
};
|
||||
|
||||
plugins.embeddable.registerReactEmbeddableSavedObject<SavedSearchAttributes>({
|
||||
plugins.embeddable.registerAddFromLibraryType<SavedSearchAttributes>({
|
||||
onAdd: async (container, savedObject) => {
|
||||
const services = await getDiscoverServicesForEmbeddable();
|
||||
const initialState = await deserializeState({
|
||||
|
@ -429,7 +429,6 @@ export class DiscoverPlugin
|
|||
initialState,
|
||||
});
|
||||
},
|
||||
embeddableType: SEARCH_EMBEDDABLE_TYPE,
|
||||
savedObjectType: SavedSearchType,
|
||||
savedObjectName: i18n.translate('discover.savedSearch.savedObjectName', {
|
||||
defaultMessage: 'Saved search',
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
|
||||
import { AddFromLibraryFlyout } from './add_from_library_flyout';
|
||||
import { usageCollection } from '../kibana_services';
|
||||
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
|
||||
import { registerAddFromLibraryType } from './registry';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { HasType } from '@kbn/presentation-publishing';
|
||||
|
||||
// Mock saved objects finder component so we can call the onChoose method.
|
||||
jest.mock('@kbn/saved-objects-finder-plugin/public', () => {
|
||||
return {
|
||||
SavedObjectFinder: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
({
|
||||
onChoose,
|
||||
}: {
|
||||
onChoose: (id: string, type: string, name: string, so: unknown) => Promise<void>;
|
||||
}) => (
|
||||
<>
|
||||
<button
|
||||
id="soFinderAddButton"
|
||||
data-test-subj="soFinderAddButton"
|
||||
onClick={() =>
|
||||
onChoose?.(
|
||||
'awesomeId',
|
||||
'AWESOME_EMBEDDABLE',
|
||||
'Awesome sauce',
|
||||
{} as unknown as SavedObjectCommon
|
||||
)
|
||||
}
|
||||
>
|
||||
Add embeddable!
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('add from library flyout', () => {
|
||||
let container: PresentationContainer & HasType;
|
||||
const onAdd = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
registerAddFromLibraryType({
|
||||
onAdd,
|
||||
savedObjectType: 'AWESOME_EMBEDDABLE',
|
||||
savedObjectName: 'Awesome sauce',
|
||||
getIconForSavedObject: () => 'happyface',
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onAdd.mockClear();
|
||||
container = {
|
||||
type: 'DASHBOARD_CONTAINER',
|
||||
...getMockPresentationContainer(),
|
||||
};
|
||||
});
|
||||
|
||||
test('renders SavedObjectFinder', async () => {
|
||||
const { container: componentContainer } = render(
|
||||
<AddFromLibraryFlyout container={container} />
|
||||
);
|
||||
|
||||
// component should not contain an extra flyout
|
||||
// https://github.com/elastic/kibana/issues/64789
|
||||
const flyout = componentContainer.querySelector('.euiFlyout');
|
||||
expect(flyout).toBeNull();
|
||||
const dummyButton = screen.queryAllByTestId('soFinderAddButton');
|
||||
expect(dummyButton).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('calls the registered onAdd method', async () => {
|
||||
render(<AddFromLibraryFlyout container={container} />);
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(onAdd).toHaveBeenCalledWith(container, {});
|
||||
});
|
||||
|
||||
test('runs telemetry function on add embeddable', async () => {
|
||||
render(<AddFromLibraryFlyout container={container} />);
|
||||
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'DASHBOARD_CONTAINER',
|
||||
'click',
|
||||
'AWESOME_EMBEDDABLE:add'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import {
|
||||
SavedObjectFinder,
|
||||
SavedObjectFinderProps,
|
||||
type SavedObjectMetaData,
|
||||
} from '@kbn/saved-objects-finder-plugin/public';
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { apiHasType } from '@kbn/presentation-publishing';
|
||||
import { CanAddNewPanel } from '@kbn/presentation-containers';
|
||||
import {
|
||||
core,
|
||||
savedObjectsTaggingOss,
|
||||
contentManagement,
|
||||
usageCollection,
|
||||
} from '../kibana_services';
|
||||
import { EmbeddableFactoryNotFoundError } from '../lib';
|
||||
import { getAddFromLibraryType, useAddFromLibraryTypes } from './registry';
|
||||
|
||||
const runAddTelemetry = (
|
||||
parent: unknown,
|
||||
savedObject: SavedObjectCommon,
|
||||
savedObjectMetaData: SavedObjectMetaData
|
||||
) => {
|
||||
if (!apiHasType(parent)) return;
|
||||
const type = savedObjectMetaData.getSavedObjectSubType
|
||||
? savedObjectMetaData.getSavedObjectSubType(savedObject)
|
||||
: savedObjectMetaData.type;
|
||||
|
||||
usageCollection?.reportUiCounter?.(parent.type, METRIC_TYPE.CLICK, `${type}:add`);
|
||||
};
|
||||
|
||||
export const AddFromLibraryFlyout = ({
|
||||
container,
|
||||
modalTitleId,
|
||||
}: {
|
||||
container: CanAddNewPanel;
|
||||
modalTitleId?: string;
|
||||
}) => {
|
||||
const libraryTypes = useAddFromLibraryTypes();
|
||||
|
||||
const onChoose: SavedObjectFinderProps['onChoose'] = useCallback(
|
||||
async (
|
||||
id: SavedObjectCommon['id'],
|
||||
type: SavedObjectCommon['type'],
|
||||
name: string,
|
||||
savedObject: SavedObjectCommon
|
||||
) => {
|
||||
const libraryType = getAddFromLibraryType(type);
|
||||
if (!libraryType) {
|
||||
core.notifications.toasts.addWarning(new EmbeddableFactoryNotFoundError(type).message);
|
||||
return;
|
||||
}
|
||||
|
||||
libraryType.onAdd(container, savedObject);
|
||||
runAddTelemetry(container, savedObject, libraryType.savedObjectMetaData);
|
||||
},
|
||||
[container]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id={modalTitleId}>
|
||||
{i18n.translate('embeddableApi.addPanel.Title', { defaultMessage: 'Add from library' })}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<SavedObjectFinder
|
||||
id="embeddableAddPanel"
|
||||
services={{
|
||||
contentClient: contentManagement.client,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
uiSettings: core.uiSettings,
|
||||
}}
|
||||
onChoose={onChoose}
|
||||
savedObjectMetaData={libraryTypes}
|
||||
showFilter={true}
|
||||
noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', {
|
||||
defaultMessage: 'No matching objects found.',
|
||||
})}
|
||||
getTooltipText={(item) => {
|
||||
return item.managed
|
||||
? i18n.translate('embeddableApi.addPanel.managedPanelTooltip', {
|
||||
defaultMessage:
|
||||
'Elastic manages this panel. Adding it to a dashboard unlinks it from the library.',
|
||||
})
|
||||
: undefined;
|
||||
}}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -17,32 +17,25 @@ import { CanAddNewPanel } from '@kbn/presentation-containers';
|
|||
import { core } from '../kibana_services';
|
||||
|
||||
const LazyAddPanelFlyout = React.lazy(async () => {
|
||||
const module = await import('./add_panel_flyout');
|
||||
return { default: module.AddPanelFlyout };
|
||||
const module = await import('./add_from_library_flyout');
|
||||
return { default: module.AddFromLibraryFlyout };
|
||||
});
|
||||
|
||||
const htmlId = htmlIdGenerator('modalTitleId');
|
||||
|
||||
export const openAddPanelFlyout = ({
|
||||
export const openAddFromLibraryFlyout = ({
|
||||
container,
|
||||
onAddPanel,
|
||||
onClose,
|
||||
}: {
|
||||
container: CanAddNewPanel;
|
||||
onAddPanel?: (id: string) => void;
|
||||
onClose?: () => void;
|
||||
}): OverlayRef => {
|
||||
const modalTitleId = htmlId();
|
||||
|
||||
// send the overlay ref to the root embeddable if it is capable of tracking overlays
|
||||
const flyoutSession = core.overlays.openFlyout(
|
||||
const flyoutRef = core.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<LazyAddPanelFlyout
|
||||
container={container}
|
||||
onAddPanel={onAddPanel}
|
||||
modalTitleId={modalTitleId}
|
||||
/>
|
||||
<LazyAddPanelFlyout container={container} modalTitleId={modalTitleId} />
|
||||
</Suspense>,
|
||||
core
|
||||
),
|
||||
|
@ -60,5 +53,5 @@ export const openAddPanelFlyout = ({
|
|||
}
|
||||
);
|
||||
|
||||
return flyoutSession;
|
||||
return flyoutRef;
|
||||
};
|
|
@ -8,54 +8,43 @@
|
|||
*/
|
||||
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CanAddNewPanel } from '@kbn/presentation-containers';
|
||||
import { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type SOToEmbeddable<TSavedObjectAttributes extends FinderAttributes = FinderAttributes> = (
|
||||
container: CanAddNewPanel,
|
||||
savedObject: SavedObjectCommon<TSavedObjectAttributes>
|
||||
) => void;
|
||||
|
||||
export type ReactEmbeddableSavedObject<
|
||||
TSavedObjectAttributes extends FinderAttributes = FinderAttributes
|
||||
> = {
|
||||
onAdd: SOToEmbeddable<TSavedObjectAttributes>;
|
||||
export type RegistryItem<TSavedObjectAttributes extends FinderAttributes = FinderAttributes> = {
|
||||
onAdd: (
|
||||
container: CanAddNewPanel,
|
||||
savedObject: SavedObjectCommon<TSavedObjectAttributes>
|
||||
) => void;
|
||||
savedObjectMetaData: SavedObjectMetaData;
|
||||
};
|
||||
|
||||
const registry: Map<string, ReactEmbeddableSavedObject<any>> = new Map();
|
||||
const registry: Map<string, RegistryItem<any>> = new Map();
|
||||
|
||||
export const registerReactEmbeddableSavedObject = <
|
||||
TSavedObjectAttributes extends FinderAttributes
|
||||
>({
|
||||
export const registerAddFromLibraryType = <TSavedObjectAttributes extends FinderAttributes>({
|
||||
onAdd,
|
||||
embeddableType,
|
||||
savedObjectType,
|
||||
savedObjectName,
|
||||
getIconForSavedObject,
|
||||
getSavedObjectSubType,
|
||||
getTooltipForSavedObject,
|
||||
}: {
|
||||
onAdd: SOToEmbeddable<TSavedObjectAttributes>;
|
||||
embeddableType: string;
|
||||
onAdd: RegistryItem['onAdd'];
|
||||
savedObjectType: string;
|
||||
savedObjectName: string;
|
||||
getIconForSavedObject: (savedObject: SavedObjectCommon<TSavedObjectAttributes>) => IconType;
|
||||
getSavedObjectSubType?: (savedObject: SavedObjectCommon<TSavedObjectAttributes>) => string;
|
||||
getTooltipForSavedObject?: (savedObject: SavedObjectCommon<TSavedObjectAttributes>) => string;
|
||||
}) => {
|
||||
if (registry.has(embeddableType)) {
|
||||
if (registry.has(savedObjectType)) {
|
||||
throw new Error(
|
||||
i18n.translate('embeddableApi.embeddableSavedObjectRegistry.keyAlreadyExistsError', {
|
||||
defaultMessage: `Embeddable type {embeddableType} already exists in the registry.`,
|
||||
values: { embeddableType },
|
||||
})
|
||||
`Saved object type '${savedObjectType}' already exists in the 'Add from Library' registry.`
|
||||
);
|
||||
}
|
||||
|
||||
registry.set(embeddableType, {
|
||||
registry.set(savedObjectType, {
|
||||
onAdd,
|
||||
savedObjectMetaData: {
|
||||
name: savedObjectName,
|
||||
|
@ -67,10 +56,14 @@ export const registerReactEmbeddableSavedObject = <
|
|||
});
|
||||
};
|
||||
|
||||
export const getReactEmbeddableSavedObjects = <
|
||||
TSavedObjectAttributes extends FinderAttributes
|
||||
>() => {
|
||||
return registry.entries() as IterableIterator<
|
||||
[string, ReactEmbeddableSavedObject<TSavedObjectAttributes>]
|
||||
>;
|
||||
export function useAddFromLibraryTypes() {
|
||||
return useMemo(() => {
|
||||
return [...registry.entries()]
|
||||
.map(([type, registryItem]) => registryItem.savedObjectMetaData)
|
||||
.sort((a, b) => a.type.localeCompare(b.type));
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const getAddFromLibraryType = (type: string) => {
|
||||
return registry.get(type);
|
||||
};
|
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
|
||||
import { AddPanelFlyout } from './add_panel_flyout';
|
||||
import { core, embeddableStart, usageCollection } from '../kibana_services';
|
||||
import { ContactCardEmbeddableFactory } from '../lib/test_samples';
|
||||
import { Container, registerReactEmbeddableSavedObject } from '../lib';
|
||||
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
|
||||
|
||||
// Mock saved objects finder component so we can call the onChoose method.
|
||||
jest.mock('@kbn/saved-objects-finder-plugin/public', () => {
|
||||
return {
|
||||
SavedObjectFinder: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
({
|
||||
onChoose,
|
||||
}: {
|
||||
onChoose: (id: string, type: string, name: string, so: unknown) => Promise<void>;
|
||||
}) => (
|
||||
<>
|
||||
<button
|
||||
id="soFinderAddButton"
|
||||
data-test-subj="soFinderAddButton"
|
||||
onClick={() =>
|
||||
onChoose?.(
|
||||
'awesomeId',
|
||||
'AWESOME_EMBEDDABLE',
|
||||
'Awesome sauce',
|
||||
{} as unknown as SavedObjectCommon
|
||||
)
|
||||
}
|
||||
>
|
||||
Add embeddable!
|
||||
</button>
|
||||
<button
|
||||
id="soFinderAddLegacyButton"
|
||||
data-test-subj="soFinderAddLegacyButton"
|
||||
onClick={() =>
|
||||
onChoose?.(
|
||||
'testId',
|
||||
'CONTACT_CARD_EMBEDDABLE',
|
||||
'test name',
|
||||
{} as unknown as SavedObjectCommon
|
||||
)
|
||||
}
|
||||
>
|
||||
Add legacy embeddable!
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('add panel flyout', () => {
|
||||
describe('registered embeddables', () => {
|
||||
let container: Container;
|
||||
const onAdd = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
registerReactEmbeddableSavedObject({
|
||||
onAdd,
|
||||
embeddableType: 'AWESOME_EMBEDDABLE',
|
||||
savedObjectType: 'AWESOME_EMBEDDABLE',
|
||||
savedObjectName: 'Awesome sauce',
|
||||
getIconForSavedObject: () => 'happyface',
|
||||
});
|
||||
|
||||
embeddableStart.getEmbeddableFactories = jest.fn().mockReturnValue([]);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onAdd.mockClear();
|
||||
container = getMockPresentationContainer() as unknown as Container;
|
||||
// @ts-ignore type is only expected on a dashboard container
|
||||
container.type = 'DASHBOARD_CONTAINER';
|
||||
});
|
||||
|
||||
test('add panel flyout renders SavedObjectFinder', async () => {
|
||||
const { container: componentContainer } = render(<AddPanelFlyout container={container} />);
|
||||
|
||||
// component should not contain an extra flyout
|
||||
// https://github.com/elastic/kibana/issues/64789
|
||||
const flyout = componentContainer.querySelector('.euiFlyout');
|
||||
expect(flyout).toBeNull();
|
||||
const dummyButton = screen.queryAllByTestId('soFinderAddButton');
|
||||
expect(dummyButton).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('add panel calls the registered onAdd method', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(onAdd).toHaveBeenCalledWith(container, {});
|
||||
});
|
||||
|
||||
test('runs telemetry function on add embeddable', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'DASHBOARD_CONTAINER',
|
||||
'click',
|
||||
'AWESOME_EMBEDDABLE:add'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy embeddables', () => {
|
||||
let container: Container;
|
||||
|
||||
beforeEach(() => {
|
||||
const coreStart = coreMock.createStart();
|
||||
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
|
||||
(() => null) as any,
|
||||
coreStart
|
||||
);
|
||||
|
||||
embeddableStart.getEmbeddableFactories = jest
|
||||
.fn()
|
||||
.mockReturnValue([contactCardEmbeddableFactory]);
|
||||
|
||||
container = getMockPresentationContainer() as unknown as Container;
|
||||
container.addNewEmbeddable = jest.fn().mockResolvedValue({ id: 'foo' });
|
||||
// @ts-ignore type is only expected on a dashboard container
|
||||
container.type = 'HELLO_WORLD_CONTAINER';
|
||||
});
|
||||
|
||||
test('add panel flyout renders SavedObjectFinder', async () => {
|
||||
const { container: componentContainer } = render(<AddPanelFlyout container={container} />);
|
||||
|
||||
// component should not contain an extra flyout
|
||||
// https://github.com/elastic/kibana/issues/64789
|
||||
const flyout = componentContainer.querySelector('.euiFlyout');
|
||||
expect(flyout).toBeNull();
|
||||
const dummyButton = screen.queryAllByTestId('soFinderAddLegacyButton');
|
||||
expect(dummyButton).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('add panel adds legacy embeddable to container', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddLegacyButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(container.addNewEmbeddable).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('shows a success toast on add', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddLegacyButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(core.notifications.toasts.addSuccess).toHaveBeenCalledWith({
|
||||
'data-test-subj': 'addObjectToContainerSuccess',
|
||||
title: 'test name was added',
|
||||
});
|
||||
});
|
||||
|
||||
test('runs telemetry function on add legacy embeddable', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddLegacyButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'HELLO_WORLD_CONTAINER',
|
||||
'click',
|
||||
'CONTACT_CARD_EMBEDDABLE:add'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import {
|
||||
SavedObjectFinder,
|
||||
SavedObjectFinderProps,
|
||||
type SavedObjectMetaData,
|
||||
} from '@kbn/saved-objects-finder-plugin/public';
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { apiHasType } from '@kbn/presentation-publishing';
|
||||
import { Toast } from '@kbn/core/public';
|
||||
import { CanAddNewPanel } from '@kbn/presentation-containers';
|
||||
import {
|
||||
core,
|
||||
embeddableStart,
|
||||
savedObjectsTaggingOss,
|
||||
contentManagement,
|
||||
usageCollection,
|
||||
} from '../kibana_services';
|
||||
import { savedObjectToPanel } from '../registry/saved_object_to_panel_methods';
|
||||
import {
|
||||
ReactEmbeddableSavedObject,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
getReactEmbeddableSavedObjects,
|
||||
PanelIncompatibleError,
|
||||
} from '../lib';
|
||||
|
||||
type LegacyFactoryMap = { [key: string]: EmbeddableFactory };
|
||||
type FactoryMap<TSavedObjectAttributes extends FinderAttributes = FinderAttributes> = {
|
||||
[key: string]: ReactEmbeddableSavedObject<TSavedObjectAttributes> & { type: string };
|
||||
};
|
||||
|
||||
type CanAddNewEmbeddable = {
|
||||
addNewEmbeddable: (type: string, explicitInput: unknown, attributes: unknown) => { id: string };
|
||||
};
|
||||
|
||||
const apiCanAddNewEmbeddable = (api: unknown): api is CanAddNewEmbeddable => {
|
||||
return typeof (api as CanAddNewEmbeddable).addNewEmbeddable === 'function';
|
||||
};
|
||||
|
||||
let lastToast: string | Toast;
|
||||
const showSuccessToast = (name: string) => {
|
||||
if (lastToast) core.notifications.toasts.remove(lastToast);
|
||||
|
||||
lastToast = core.notifications.toasts.addSuccess({
|
||||
title: i18n.translate('embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle', {
|
||||
defaultMessage: '{savedObjectName} was added',
|
||||
values: {
|
||||
savedObjectName: name,
|
||||
},
|
||||
}),
|
||||
'data-test-subj': 'addObjectToContainerSuccess',
|
||||
});
|
||||
};
|
||||
|
||||
const runAddTelemetry = (
|
||||
parent: unknown,
|
||||
factoryType: string,
|
||||
savedObject: SavedObjectCommon,
|
||||
savedObjectMetaData?: SavedObjectMetaData
|
||||
) => {
|
||||
if (!apiHasType(parent)) return;
|
||||
const type = savedObjectMetaData?.getSavedObjectSubType
|
||||
? savedObjectMetaData.getSavedObjectSubType(savedObject)
|
||||
: factoryType;
|
||||
|
||||
usageCollection?.reportUiCounter?.(parent.type, METRIC_TYPE.CLICK, `${type}:add`);
|
||||
};
|
||||
|
||||
export const AddPanelFlyout = ({
|
||||
container,
|
||||
onAddPanel,
|
||||
modalTitleId,
|
||||
}: {
|
||||
container: CanAddNewPanel;
|
||||
onAddPanel?: (id: string) => void;
|
||||
modalTitleId?: string;
|
||||
}) => {
|
||||
const legacyFactoriesBySavedObjectType: LegacyFactoryMap = useMemo(() => {
|
||||
return [...embeddableStart.getEmbeddableFactories()]
|
||||
.filter(
|
||||
(embeddableFactory) =>
|
||||
Boolean(embeddableFactory.savedObjectMetaData?.type) && !embeddableFactory.isContainerType
|
||||
)
|
||||
.reduce((acc, factory) => {
|
||||
acc[factory.savedObjectMetaData!.type] = factory;
|
||||
return acc;
|
||||
}, {} as LegacyFactoryMap);
|
||||
}, []);
|
||||
|
||||
const factoriesBySavedObjectType: FactoryMap = useMemo(() => {
|
||||
return [...getReactEmbeddableSavedObjects()]
|
||||
.filter(([type, embeddableFactory]) => {
|
||||
return Boolean(embeddableFactory.savedObjectMetaData?.type);
|
||||
})
|
||||
.reduce((acc, [type, factory]) => {
|
||||
acc[factory.savedObjectMetaData!.type] = {
|
||||
...factory,
|
||||
type,
|
||||
};
|
||||
return acc;
|
||||
}, {} as FactoryMap);
|
||||
}, []);
|
||||
|
||||
const metaData = useMemo(
|
||||
() =>
|
||||
[
|
||||
...Object.values(factoriesBySavedObjectType),
|
||||
...Object.values(legacyFactoriesBySavedObjectType),
|
||||
]
|
||||
.filter((embeddableFactory) => Boolean(embeddableFactory.savedObjectMetaData))
|
||||
.map(({ savedObjectMetaData }) => savedObjectMetaData!)
|
||||
.sort((a, b) => a.type.localeCompare(b.type)),
|
||||
[factoriesBySavedObjectType, legacyFactoriesBySavedObjectType]
|
||||
);
|
||||
|
||||
const onChoose: SavedObjectFinderProps['onChoose'] = useCallback(
|
||||
async (
|
||||
id: SavedObjectCommon['id'],
|
||||
type: SavedObjectCommon['type'],
|
||||
name: string,
|
||||
savedObject: SavedObjectCommon
|
||||
) => {
|
||||
if (factoriesBySavedObjectType[type]) {
|
||||
const factory = factoriesBySavedObjectType[type];
|
||||
const { onAdd, savedObjectMetaData } = factory;
|
||||
|
||||
onAdd(container, savedObject);
|
||||
runAddTelemetry(container, factory.type, savedObject, savedObjectMetaData);
|
||||
return;
|
||||
}
|
||||
|
||||
const legacyFactoryForSavedObjectType = legacyFactoriesBySavedObjectType[type];
|
||||
if (!legacyFactoryForSavedObjectType) {
|
||||
throw new EmbeddableFactoryNotFoundError(type);
|
||||
}
|
||||
|
||||
// container.addNewEmbeddable is required for legacy embeddables to support
|
||||
// panel placement strategies
|
||||
if (!apiCanAddNewEmbeddable(container)) {
|
||||
throw new PanelIncompatibleError();
|
||||
}
|
||||
|
||||
let embeddableId: string;
|
||||
|
||||
if (savedObjectToPanel[type]) {
|
||||
// this panel type has a custom method for converting saved objects to panels
|
||||
const panel = savedObjectToPanel[type](savedObject);
|
||||
|
||||
const { id: _embeddableId } = await container.addNewEmbeddable(
|
||||
legacyFactoryForSavedObjectType.type,
|
||||
panel,
|
||||
savedObject.attributes
|
||||
);
|
||||
|
||||
embeddableId = _embeddableId;
|
||||
} else {
|
||||
const { id: _embeddableId } = await container.addNewEmbeddable(
|
||||
legacyFactoryForSavedObjectType.type,
|
||||
{ savedObjectId: id },
|
||||
savedObject.attributes
|
||||
);
|
||||
|
||||
embeddableId = _embeddableId;
|
||||
}
|
||||
|
||||
onAddPanel?.(embeddableId);
|
||||
|
||||
showSuccessToast(name);
|
||||
const { savedObjectMetaData, type: factoryType } = legacyFactoryForSavedObjectType;
|
||||
runAddTelemetry(container, factoryType, savedObject, savedObjectMetaData);
|
||||
},
|
||||
[container, factoriesBySavedObjectType, legacyFactoriesBySavedObjectType, onAddPanel]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id={modalTitleId}>
|
||||
{i18n.translate('embeddableApi.addPanel.Title', { defaultMessage: 'Add from library' })}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<SavedObjectFinder
|
||||
id="embeddableAddPanel"
|
||||
services={{
|
||||
contentClient: contentManagement.client,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
uiSettings: core.uiSettings,
|
||||
}}
|
||||
onChoose={onChoose}
|
||||
savedObjectMetaData={metaData}
|
||||
showFilter={true}
|
||||
noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', {
|
||||
defaultMessage: 'No matching objects found.',
|
||||
})}
|
||||
getTooltipText={(item) => {
|
||||
return item.managed
|
||||
? i18n.translate('embeddableApi.addPanel.managedPanelTooltip', {
|
||||
defaultMessage:
|
||||
'Elastic manages this panel. Adding it to a dashboard unlinks it from the library.',
|
||||
})
|
||||
: undefined;
|
||||
}}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -10,7 +10,8 @@
|
|||
import { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { EmbeddablePublicPlugin } from './plugin';
|
||||
|
||||
export { openAddPanelFlyout } from './add_panel_flyout/open_add_panel_flyout';
|
||||
export { useAddFromLibraryTypes } from './add_from_library/registry';
|
||||
export { openAddFromLibraryFlyout } from './add_from_library/open_add_from_library_flyout';
|
||||
export { EmbeddablePanel } from './embeddable_panel';
|
||||
export {
|
||||
cellValueTrigger,
|
||||
|
@ -80,7 +81,6 @@ export type {
|
|||
PanelState,
|
||||
PropertySpec,
|
||||
RangeSelectContext,
|
||||
ReactEmbeddableSavedObject,
|
||||
ReferenceOrValueEmbeddable,
|
||||
SavedObjectEmbeddableInput,
|
||||
SelfStyledEmbeddable,
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export {
|
||||
type ReactEmbeddableSavedObject,
|
||||
getReactEmbeddableSavedObjects,
|
||||
registerReactEmbeddableSavedObject,
|
||||
} from './embeddable_saved_object_registry';
|
|
@ -17,4 +17,3 @@ export * from './reference_or_value_embeddable';
|
|||
export * from './self_styled_embeddable';
|
||||
export * from './filterable_embeddable';
|
||||
export * from './factory_migrations/run_factory_migrations';
|
||||
export * from './embeddable_saved_object_registry';
|
||||
|
|
|
@ -33,14 +33,13 @@ import {
|
|||
SelfStyledEmbeddable,
|
||||
} from '.';
|
||||
import { setKibanaServices } from './kibana_services';
|
||||
import { registerReactEmbeddableSavedObject } from './lib';
|
||||
import { SelfStyledOptions } from './lib/self_styled_embeddable/types';
|
||||
import { EmbeddablePublicPlugin } from './plugin';
|
||||
import {
|
||||
reactEmbeddableRegistryHasKey,
|
||||
registerReactEmbeddableFactory,
|
||||
} from './react_embeddable_system';
|
||||
import { registerSavedObjectToPanelMethod } from './registry/saved_object_to_panel_methods';
|
||||
import { registerAddFromLibraryType } from './add_from_library/registry';
|
||||
|
||||
export { mockAttributeService } from './lib/attribute_service/attribute_service.mock';
|
||||
export type Setup = jest.Mocked<EmbeddableSetup>;
|
||||
|
@ -100,12 +99,7 @@ export function mockFilterableEmbeddable<OriginalEmbeddableType>(
|
|||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
registerSavedObjectToPanelMethod: jest
|
||||
.fn()
|
||||
.mockImplementation(registerSavedObjectToPanelMethod),
|
||||
registerReactEmbeddableSavedObject: jest
|
||||
.fn()
|
||||
.mockImplementation(registerReactEmbeddableSavedObject),
|
||||
registerAddFromLibraryType: jest.fn().mockImplementation(registerAddFromLibraryType),
|
||||
registerReactEmbeddableFactory: jest.fn().mockImplementation(registerReactEmbeddableFactory),
|
||||
registerEmbeddableFactory: jest.fn(),
|
||||
registerEnhancement: jest.fn(),
|
||||
|
@ -117,7 +111,6 @@ const createSetupContract = (): Setup => {
|
|||
const createStartContract = (): Start => {
|
||||
const startContract: Start = {
|
||||
reactEmbeddableRegistryHasKey: jest.fn().mockImplementation(reactEmbeddableRegistryHasKey),
|
||||
getReactEmbeddableSavedObjects: jest.fn(),
|
||||
getEmbeddableFactories: jest.fn(),
|
||||
getEmbeddableFactory: jest.fn(),
|
||||
telemetry: jest.fn(),
|
||||
|
|
|
@ -25,7 +25,6 @@ import { migrateToLatest, PersistableStateService } from '@kbn/kibana-utils-plug
|
|||
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import {
|
||||
EmbeddableFactoryRegistry,
|
||||
EmbeddableFactoryProvider,
|
||||
|
@ -41,9 +40,6 @@ import {
|
|||
defaultEmbeddableFactoryProvider,
|
||||
IEmbeddable,
|
||||
SavedObjectEmbeddableInput,
|
||||
registerReactEmbeddableSavedObject,
|
||||
ReactEmbeddableSavedObject,
|
||||
getReactEmbeddableSavedObjects,
|
||||
} from './lib';
|
||||
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
|
||||
import { EmbeddableStateTransfer } from './lib/state_transfer';
|
||||
|
@ -62,7 +58,7 @@ import {
|
|||
reactEmbeddableRegistryHasKey,
|
||||
registerReactEmbeddableFactory,
|
||||
} from './react_embeddable_system';
|
||||
import { registerSavedObjectToPanelMethod } from './registry/saved_object_to_panel_methods';
|
||||
import { registerAddFromLibraryType } from './add_from_library/registry';
|
||||
|
||||
export interface EmbeddableSetupDependencies {
|
||||
uiActions: UiActionsSetup;
|
||||
|
@ -79,17 +75,16 @@ export interface EmbeddableStartDependencies {
|
|||
|
||||
export interface EmbeddableSetup {
|
||||
/**
|
||||
* Register an embeddable API saved object with the Add from library flyout.
|
||||
* Register a saved object type with the "Add from library" flyout.
|
||||
*
|
||||
* @example
|
||||
* registerReactEmbeddableSavedObject({
|
||||
* registerAddFromLibraryType({
|
||||
* onAdd: (container, savedObject) => {
|
||||
* container.addNewPanel({
|
||||
* panelType: CONTENT_ID,
|
||||
* initialState: savedObject.attributes,
|
||||
* });
|
||||
* },
|
||||
* embeddableType: CONTENT_ID,
|
||||
* savedObjectType: MAP_SAVED_OBJECT_TYPE,
|
||||
* savedObjectName: i18n.translate('xpack.maps.mapSavedObjectLabel', {
|
||||
* defaultMessage: 'Map',
|
||||
|
@ -97,12 +92,7 @@ export interface EmbeddableSetup {
|
|||
* getIconForSavedObject: () => APP_ICON,
|
||||
* });
|
||||
*/
|
||||
registerReactEmbeddableSavedObject: typeof registerReactEmbeddableSavedObject;
|
||||
|
||||
/**
|
||||
* @deprecated React embeddables should register their saved objects with {@link registerReactEmbeddableSavedObject}.
|
||||
*/
|
||||
registerSavedObjectToPanelMethod: typeof registerSavedObjectToPanelMethod;
|
||||
registerAddFromLibraryType: typeof registerAddFromLibraryType;
|
||||
|
||||
/**
|
||||
* Registers an async {@link ReactEmbeddableFactory} getter.
|
||||
|
@ -136,14 +126,6 @@ export interface EmbeddableStart extends PersistableStateService<EmbeddableState
|
|||
*/
|
||||
reactEmbeddableRegistryHasKey: (type: string) => boolean;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns An iterator over all {@link ReactEmbeddableSavedObject}s that have been registered using {@link registerReactEmbeddableSavedObject}.
|
||||
*/
|
||||
getReactEmbeddableSavedObjects: <
|
||||
TSavedObjectAttributes extends FinderAttributes
|
||||
>() => IterableIterator<[string, ReactEmbeddableSavedObject<TSavedObjectAttributes>]>;
|
||||
|
||||
/**
|
||||
* @deprecated use {@link registerReactEmbeddableFactory} instead.
|
||||
*/
|
||||
|
@ -192,8 +174,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
|
||||
return {
|
||||
registerReactEmbeddableFactory,
|
||||
registerSavedObjectToPanelMethod,
|
||||
registerReactEmbeddableSavedObject,
|
||||
registerAddFromLibraryType,
|
||||
|
||||
registerEmbeddableFactory: this.registerEmbeddableFactory,
|
||||
registerEnhancement: this.registerEnhancement,
|
||||
|
@ -244,7 +225,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
|
||||
const embeddableStart: EmbeddableStart = {
|
||||
reactEmbeddableRegistryHasKey,
|
||||
getReactEmbeddableSavedObjects,
|
||||
|
||||
getEmbeddableFactory: this.getEmbeddableFactory,
|
||||
getEmbeddableFactories: this.getEmbeddableFactories,
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
|
||||
type SavedObjectToPanelMethod<TSavedObjectAttributes, TByValueInput> = (
|
||||
// @ts-expect-error upgrade typescript v4.9.5
|
||||
savedObject: SavedObjectCommon<TSavedObjectAttributes>
|
||||
) => { savedObjectId: string } | Partial<TByValueInput>;
|
||||
|
||||
export const savedObjectToPanel: Record<string, SavedObjectToPanelMethod<any, any>> = {};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* React embeddables should register their saved object types with the registerReactEmbeddableSavedObject registry.
|
||||
*/
|
||||
export const registerSavedObjectToPanelMethod = <TSavedObjectAttributes, TByValueAttributes>(
|
||||
savedObjectType: string,
|
||||
method: SavedObjectToPanelMethod<TSavedObjectAttributes, TByValueAttributes>
|
||||
) => {
|
||||
savedObjectToPanel[savedObjectType] = method;
|
||||
};
|
|
@ -61,7 +61,7 @@ export class LinksPlugin
|
|||
name: APP_NAME,
|
||||
});
|
||||
|
||||
plugins.embeddable.registerReactEmbeddableSavedObject({
|
||||
plugins.embeddable.registerAddFromLibraryType({
|
||||
onAdd: async (container, savedObject) => {
|
||||
const initialState = await deserializeLinksSavedObject(savedObject);
|
||||
container.addNewPanel<LinksRuntimeState>({
|
||||
|
@ -69,7 +69,6 @@ export class LinksPlugin
|
|||
initialState,
|
||||
});
|
||||
},
|
||||
embeddableType: CONTENT_ID,
|
||||
savedObjectType: CONTENT_ID,
|
||||
savedObjectName: APP_NAME,
|
||||
getIconForSavedObject: () => APP_ICON,
|
||||
|
|
|
@ -411,14 +411,13 @@ export class VisualizationsPlugin
|
|||
const getVisualizeEmbeddableFactory = await getVisualizeEmbeddableFactoryLazy();
|
||||
return getVisualizeEmbeddableFactory({ embeddableStart, embeddableEnhancedStart });
|
||||
});
|
||||
embeddable.registerReactEmbeddableSavedObject<VisualizationSavedObjectAttributes>({
|
||||
embeddable.registerAddFromLibraryType<VisualizationSavedObjectAttributes>({
|
||||
onAdd: (container, savedObject) => {
|
||||
container.addNewPanel<VisualizeSerializedState>({
|
||||
panelType: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
initialState: { savedObjectId: savedObject.id },
|
||||
});
|
||||
},
|
||||
embeddableType: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
savedObjectType: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
savedObjectName: i18n.translate('visualizations.visualizeSavedObjectName', {
|
||||
defaultMessage: 'Visualization',
|
||||
|
|
|
@ -9,14 +9,9 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eu
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
|
||||
import { EmbeddableFactory, ReactEmbeddableSavedObject } from '@kbn/embeddable-plugin/public';
|
||||
import { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { SavedObjectFinder, SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public';
|
||||
import {
|
||||
contentManagementService,
|
||||
coreServices,
|
||||
embeddableService,
|
||||
} from '../../services/kibana_services';
|
||||
import { useAddFromLibraryTypes } from '@kbn/embeddable-plugin/public';
|
||||
import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public';
|
||||
import { contentManagementService, coreServices } from '../../services/kibana_services';
|
||||
|
||||
const strings = {
|
||||
getNoItemsText: () =>
|
||||
|
@ -29,13 +24,6 @@ const strings = {
|
|||
}),
|
||||
};
|
||||
|
||||
interface LegacyFactoryMap {
|
||||
[key: string]: EmbeddableFactory;
|
||||
}
|
||||
interface FactoryMap<TSavedObjectAttributes extends FinderAttributes = FinderAttributes> {
|
||||
[key: string]: ReactEmbeddableSavedObject<TSavedObjectAttributes> & { type: string };
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
onClose: () => void;
|
||||
onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void;
|
||||
|
@ -49,81 +37,18 @@ export const AddEmbeddableFlyout: FC<Props> = ({
|
|||
onClose,
|
||||
isByValueEnabled,
|
||||
}) => {
|
||||
const legacyFactoriesBySavedObjectType: LegacyFactoryMap = useMemo(() => {
|
||||
return [...embeddableService.getEmbeddableFactories()]
|
||||
.filter(
|
||||
(embeddableFactory) =>
|
||||
Boolean(embeddableFactory.savedObjectMetaData?.type) && !embeddableFactory.isContainerType
|
||||
)
|
||||
.reduce((acc, factory) => {
|
||||
acc[factory.savedObjectMetaData!.type] = factory;
|
||||
return acc;
|
||||
}, {} as LegacyFactoryMap);
|
||||
}, []);
|
||||
const libraryTypes = useAddFromLibraryTypes();
|
||||
|
||||
const factoriesBySavedObjectType: FactoryMap = useMemo(() => {
|
||||
return [...embeddableService.getReactEmbeddableSavedObjects()]
|
||||
.filter(([type, embeddableFactory]) => {
|
||||
return Boolean(embeddableFactory.savedObjectMetaData?.type);
|
||||
})
|
||||
.reduce((acc, [type, factory]) => {
|
||||
acc[factory.savedObjectMetaData!.type] = {
|
||||
...factory,
|
||||
type,
|
||||
};
|
||||
return acc;
|
||||
}, {} as FactoryMap);
|
||||
}, []);
|
||||
|
||||
const metaData = useMemo(
|
||||
() =>
|
||||
[
|
||||
...Object.values(factoriesBySavedObjectType),
|
||||
...Object.values(legacyFactoriesBySavedObjectType),
|
||||
]
|
||||
.filter((factory) =>
|
||||
Boolean(
|
||||
factory.type !== 'links' && // Links panels only exist on Dashboards
|
||||
(isByValueEnabled || availableEmbeddables.includes(factory.type))
|
||||
)
|
||||
)
|
||||
.map((factory) => factory.savedObjectMetaData)
|
||||
.filter<SavedObjectMetaData<{}>>(function (
|
||||
maybeSavedObjectMetaData
|
||||
): maybeSavedObjectMetaData is SavedObjectMetaData<{}> {
|
||||
return maybeSavedObjectMetaData !== undefined;
|
||||
})
|
||||
.sort((a, b) => a.type.localeCompare(b.type)),
|
||||
[
|
||||
availableEmbeddables,
|
||||
factoriesBySavedObjectType,
|
||||
isByValueEnabled,
|
||||
legacyFactoriesBySavedObjectType,
|
||||
]
|
||||
);
|
||||
const canvasOnlyLibraryTypes = useMemo(() => {
|
||||
// Links panels are not supported in Canvas
|
||||
return libraryTypes.filter(({ type }) => type !== 'links');
|
||||
}, [libraryTypes]);
|
||||
|
||||
const onAddPanel = useCallback(
|
||||
(id: string, savedObjectType: string) => {
|
||||
if (factoriesBySavedObjectType[savedObjectType]) {
|
||||
const factory = factoriesBySavedObjectType[savedObjectType];
|
||||
const { type } = factory;
|
||||
onSelect(id, type, isByValueEnabled);
|
||||
return;
|
||||
}
|
||||
const embeddableFactories = embeddableService.getEmbeddableFactories();
|
||||
// Find the embeddable type from the saved object type
|
||||
const found = Array.from(embeddableFactories).find((embeddableFactory) => {
|
||||
return Boolean(
|
||||
embeddableFactory.savedObjectMetaData &&
|
||||
embeddableFactory.savedObjectMetaData.type === savedObjectType
|
||||
);
|
||||
});
|
||||
|
||||
const foundEmbeddableType = found ? found.type : 'unknown';
|
||||
|
||||
onSelect(id, foundEmbeddableType, isByValueEnabled);
|
||||
onSelect(id, savedObjectType, isByValueEnabled);
|
||||
},
|
||||
[isByValueEnabled, onSelect, factoriesBySavedObjectType]
|
||||
[isByValueEnabled, onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -137,7 +62,7 @@ export const AddEmbeddableFlyout: FC<Props> = ({
|
|||
<SavedObjectFinder
|
||||
id="canvasEmbeddableFlyout"
|
||||
onChoose={onAddPanel}
|
||||
savedObjectMetaData={metaData}
|
||||
savedObjectMetaData={canvasOnlyLibraryTypes}
|
||||
showFilter={true}
|
||||
noItemsMessage={strings.getNoItemsText()}
|
||||
services={{
|
||||
|
|
|
@ -395,7 +395,7 @@ export class LensPlugin {
|
|||
});
|
||||
|
||||
// Let Dashboard know about the Lens panel type
|
||||
embeddable.registerReactEmbeddableSavedObject<LensSavedObjectAttributes>({
|
||||
embeddable.registerAddFromLibraryType<LensSavedObjectAttributes>({
|
||||
onAdd: async (container, savedObject) => {
|
||||
const { attributeService } = await getStartServicesForEmbeddable();
|
||||
// deserialize the saved object from visualize library
|
||||
|
@ -411,7 +411,6 @@ export class LensPlugin {
|
|||
initialState: state,
|
||||
});
|
||||
},
|
||||
embeddableType: LENS_EMBEDDABLE_TYPE,
|
||||
savedObjectType: LENS_EMBEDDABLE_TYPE,
|
||||
savedObjectName: i18n.translate('xpack.lens.mapSavedObjectLabel', {
|
||||
defaultMessage: 'Lens',
|
||||
|
|
|
@ -22,14 +22,13 @@ export function setupMapEmbeddable(embeddableSetup: EmbeddableSetup) {
|
|||
return mapEmbeddableFactory;
|
||||
});
|
||||
|
||||
embeddableSetup.registerReactEmbeddableSavedObject<MapAttributes>({
|
||||
embeddableSetup.registerAddFromLibraryType<MapAttributes>({
|
||||
onAdd: (container, savedObject) => {
|
||||
container.addNewPanel({
|
||||
panelType: MAP_SAVED_OBJECT_TYPE,
|
||||
initialState: { savedObjectId: savedObject.id },
|
||||
});
|
||||
},
|
||||
embeddableType: MAP_SAVED_OBJECT_TYPE,
|
||||
savedObjectType: MAP_SAVED_OBJECT_TYPE,
|
||||
savedObjectName: i18n.translate('xpack.maps.mapSavedObjectLabel', {
|
||||
defaultMessage: 'Map',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { OverlayRef } from '@kbn/core/public';
|
||||
import { v4 } from 'uuid';
|
||||
import { openAddPanelFlyout } from '@kbn/embeddable-plugin/public';
|
||||
import { openAddFromLibraryFlyout } from '@kbn/embeddable-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
|
@ -107,7 +107,7 @@ export function AddFromLibraryButton({ onItemAdd }: AddFromLibraryButtonProps) {
|
|||
data-test-subj="investigateAppAddFromLibraryButtonImportFromLibraryButton"
|
||||
iconType="importAction"
|
||||
onClick={() => {
|
||||
panelRef.current = openAddPanelFlyout({
|
||||
panelRef.current = openAddFromLibraryFlyout({
|
||||
container,
|
||||
});
|
||||
|
||||
|
|
|
@ -2860,7 +2860,6 @@
|
|||
"domDragDrop.keyboardInstructionsReorder": "Appuyez sur la barre d'espace ou sur Entrée pour commencer à faire glisser. Lors du glissement, utilisez les touches fléchées haut/bas pour réorganiser les éléments dans le groupe et les touches gauche/droite pour choisir les cibles de dépôt à l'extérieur du groupe. Appuyez à nouveau sur la barre d'espace ou sur Entrée pour terminer.",
|
||||
"embeddableApi.addPanel.managedPanelTooltip": "Elastic gère ce panneau. Le fait de l'ajouter à un tableau de bord le dissocie de la bibliothèque.",
|
||||
"embeddableApi.addPanel.noMatchingObjectsMessage": "Aucun objet correspondant trouvé.",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté.",
|
||||
"embeddableApi.addPanel.Title": "Ajouter depuis la bibliothèque",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "Une erreur s'est produite lors de l'enregistrement. Erreur : {errorMessage}.",
|
||||
"embeddableApi.cellValueTrigger.description": "Les actions apparaissent dans les options de valeur de cellule dans la visualisation",
|
||||
|
@ -2871,7 +2870,6 @@
|
|||
"embeddableApi.compatibility.defaultTypeDisplayName": "graphique",
|
||||
"embeddableApi.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau",
|
||||
"embeddableApi.contextMenuTrigger.title": "Menu contextuel",
|
||||
"embeddableApi.embeddableSavedObjectRegistry.keyAlreadyExistsError": "Le type incorporable {embeddableType} existe déjà dans le registre.",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.",
|
||||
"embeddableApi.errors.paneldoesNotExist": "Panneau introuvable",
|
||||
"embeddableApi.errors.panelIncompatibleError": "L'API du panneau n'est pas compatible",
|
||||
|
|
|
@ -2854,7 +2854,6 @@
|
|||
"domDragDrop.keyboardInstructionsReorder": "スペースまたはEnterを押してドラッグを開始します。ドラッグするときには、上下矢印キーを使用すると、グループの項目を並べ替えます。左右矢印キーを使用すると、グループの外側でドロップ対象を選択します。もう一度スペースまたはEnterを押すと終了します。",
|
||||
"embeddableApi.addPanel.managedPanelTooltip": "Elasticはこのパネルを管理します。ダッシュボードに追加すると、ライブラリからリンクが解除されます。",
|
||||
"embeddableApi.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} が追加されました",
|
||||
"embeddableApi.addPanel.Title": "ライブラリから追加",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "保存中にエラーが発生しました。エラー:{errorMessage}",
|
||||
"embeddableApi.cellValueTrigger.description": "アクションはビジュアライゼーションのセル値オプションに表示されます",
|
||||
|
@ -2865,7 +2864,6 @@
|
|||
"embeddableApi.compatibility.defaultTypeDisplayName": "チャート",
|
||||
"embeddableApi.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます",
|
||||
"embeddableApi.contextMenuTrigger.title": "コンテキストメニュー",
|
||||
"embeddableApi.embeddableSavedObjectRegistry.keyAlreadyExistsError": "埋め込み可能タイプ\"{embeddableType}\"はすでにレジストリに存在します。",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "{type} を読み込めません。Elasticsearch と Kibanaのデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。",
|
||||
"embeddableApi.errors.paneldoesNotExist": "パネルが見つかりません",
|
||||
"embeddableApi.errors.panelIncompatibleError": "パネルAPIに互換性がありません",
|
||||
|
|
|
@ -2889,7 +2889,6 @@
|
|||
"domDragDrop.keyboardInstructionsReorder": "按空格键或 enter 键开始拖动。拖动时,请使用上下箭头键重新排列组中的项目,使用左右箭头键在组之外选择拖动目标。再次按空格键或 enter 键结束操作。",
|
||||
"embeddableApi.addPanel.managedPanelTooltip": "Elastic 将管理此面板。将其添加到仪表板会取消其与库的链接。",
|
||||
"embeddableApi.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} 已添加",
|
||||
"embeddableApi.addPanel.Title": "从库中添加",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "保存时出错。错误:{errorMessage}",
|
||||
"embeddableApi.cellValueTrigger.description": "操作在可视化上的单元格值选项中显示",
|
||||
|
@ -2900,7 +2899,6 @@
|
|||
"embeddableApi.compatibility.defaultTypeDisplayName": "图表",
|
||||
"embeddableApi.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单",
|
||||
"embeddableApi.contextMenuTrigger.title": "上下文菜单",
|
||||
"embeddableApi.embeddableSavedObjectRegistry.keyAlreadyExistsError": "注册表中已存在可嵌入对象类型 {embeddableType}。",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。",
|
||||
"embeddableApi.errors.paneldoesNotExist": "未找到面板",
|
||||
"embeddableApi.errors.panelIncompatibleError": "面板 API 不兼容",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue