Skip to content

Commit

Permalink
[embeddable] remove legacy embeddable factories from 'Add from librar…
Browse files Browse the repository at this point in the history
…y' flyout (#202823)

Part of #180059

PR removes legacy embeddable factory support from Canvas and Dashboard
`Add from library` flyout

PR also does the following clean-ups
1) Renames folder, files, and component from `add_panel_flyout` to
`add_from_library_flyout`. When component was originally created,
dashboard `Add panel` button did not exist, and `Add from library`
button was called `Add panel`. Now that dashboard contains `Add panel`
and `Add from library` buttons, the old naming convention is super
confusing and not longer lines up with the current UI.
2) moves registry to `add_from_library` folder so that the registry is
in closer proximity to its usage.
2) Renames `registerReactEmbeddableSavedObject` to
`registerAddFromLibraryType` because
`registerReactEmbeddableSavedObject` does not clearly specifying what
the registry enables.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 5, 2024
1 parent 178baa8 commit d508b5d
Show file tree
Hide file tree
Showing 25 changed files with 284 additions and 658 deletions.
11 changes: 6 additions & 5 deletions examples/embeddable_examples/public/app/register_embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
Expand Down
3 changes: 1 addition & 2 deletions src/plugins/discover/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,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({
Expand All @@ -427,7 +427,6 @@ export class DiscoverPlugin
initialState,
});
},
embeddableType: SEARCH_EMBEDDABLE_TYPE,
savedObjectType: SavedSearchType,
savedObjectName: i18n.translate('discover.savedSearch.savedObjectName', {
defaultMessage: 'Saved search',
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
);
});
});
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
Expand All @@ -60,5 +53,5 @@ export const openAddPanelFlyout = ({
}
);

return flyoutSession;
return flyoutRef;
};
Loading

0 comments on commit d508b5d

Please sign in to comment.