diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx
index c4641ba0f68d..45841a539ece 100644
--- a/examples/embeddable_examples/public/list_container/list_container.tsx
+++ b/examples/embeddable_examples/public/list_container/list_container.tsx
@@ -17,7 +17,7 @@ export class ListContainer extends Container<{}, ContainerInput> {
public readonly type = LIST_CONTAINER;
private node?: HTMLElement;
- constructor(input: ContainerInput, private embeddableServices: EmbeddableStart) {
+ constructor(input: ContainerInput, embeddableServices: EmbeddableStart) {
super(input, { embeddableLoaded: {} }, embeddableServices.getEmbeddableFactory);
}
@@ -32,10 +32,7 @@ export class ListContainer extends Container<{}, ContainerInput> {
ReactDOM.unmountComponentAtNode(this.node);
}
this.node = node;
- ReactDOM.render(
- ,
- node
- );
+ ReactDOM.render(, node);
}
public destroy() {
diff --git a/examples/embeddable_examples/public/list_container/list_container_component.tsx b/examples/embeddable_examples/public/list_container/list_container_component.tsx
index d83ce243f37d..e0d0b7c72139 100644
--- a/examples/embeddable_examples/public/list_container/list_container_component.tsx
+++ b/examples/embeddable_examples/public/list_container/list_container_component.tsx
@@ -14,22 +14,16 @@ import {
withEmbeddableSubscription,
ContainerInput,
ContainerOutput,
- EmbeddableStart,
- EmbeddableChildPanel,
+ EmbeddablePanel,
} from '@kbn/embeddable-plugin/public';
interface Props {
embeddable: IContainer;
input: ContainerInput;
output: ContainerOutput;
- embeddableServices: EmbeddableStart;
}
-function renderList(
- embeddable: IContainer,
- panels: ContainerInput['panels'],
- embeddableServices: EmbeddableStart
-) {
+function renderList(embeddable: IContainer, panels: ContainerInput['panels']) {
let number = 0;
const list = Object.values(panels).map((panel) => {
number++;
@@ -42,10 +36,8 @@ function renderList(
- embeddable.untilEmbeddableLoaded(panel.explicitInput.id)}
/>
@@ -55,12 +47,12 @@ function renderList(
return list;
}
-export function ListContainerComponentInner({ embeddable, input, embeddableServices }: Props) {
+export function ListContainerComponentInner({ embeddable, input }: Props) {
return (
{embeddable.getTitle()}
- {renderList(embeddable, input.panels, embeddableServices)}
+ {renderList(embeddable, input.panels)}
);
}
@@ -73,6 +65,5 @@ export function ListContainerComponentInner({ embeddable, input, embeddableServi
export const ListContainerComponent = withEmbeddableSubscription<
ContainerInput,
ContainerOutput,
- IContainer,
- { embeddableServices: EmbeddableStart }
+ IContainer
>(ListContainerComponentInner);
diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx
index 2c225303fee2..39b8c4923641 100644
--- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx
+++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx
@@ -25,6 +25,8 @@ import {
ContainerOutput,
EmbeddableOutput,
EmbeddableStart,
+ EmbeddablePanel,
+ openAddPanelFlyout,
} from '@kbn/embeddable-plugin/public';
import { SearchableListContainer, SearchableContainerInput } from './searchable_list_container';
@@ -120,7 +122,7 @@ export class SearchableListContainerComponentInner extends Component
@@ -150,6 +152,17 @@ export class SearchableListContainerComponentInner extends Component
+
+
+ openAddPanelFlyout({ container: embeddable })}
+ >
+ Add panel
+
+
+
);
@@ -171,7 +184,7 @@ export class SearchableListContainerComponentInner extends Component {
const childEmbeddable = embeddable.getChild(panel.explicitInput.id);
@@ -189,7 +202,7 @@ export class SearchableListContainerComponentInner extends Component
-
+
diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx
index ff6b04eb2bd3..9efc88bf0d6f 100644
--- a/examples/embeddable_explorer/public/app.tsx
+++ b/examples/embeddable_explorer/public/app.tsx
@@ -112,7 +112,6 @@ const EmbeddableExplorerApp = ({
id: 'embeddablePanelExample',
component: (
),
diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx
index 2983aa5dc252..e571e195370c 100644
--- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx
+++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx
@@ -9,7 +9,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { EuiPanel, EuiText, EuiPageTemplate } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
-import { EmbeddableStart, IEmbeddable } from '@kbn/embeddable-plugin/public';
+import { IEmbeddable, EmbeddablePanel } from '@kbn/embeddable-plugin/public';
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
@@ -19,11 +19,10 @@ import {
} from '@kbn/embeddable-examples-plugin/public';
interface Props {
- embeddableServices: EmbeddableStart;
searchListContainerFactory: SearchableListContainerFactory;
}
-export function EmbeddablePanelExample({ embeddableServices, searchListContainerFactory }: Props) {
+export function EmbeddablePanelExample({ searchListContainerFactory }: Props) {
const searchableInput = {
id: '1',
title: 'My searchable todo list',
@@ -120,7 +119,7 @@ export function EmbeddablePanelExample({ embeddableServices, searchListContainer
{embeddable ? (
-
+
) : (
Loading...
)}
diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx
index 39ff6ebc4841..9da737a5e744 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx
@@ -10,11 +10,7 @@ import React, { useState, useRef, useEffect, useLayoutEffect } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import classNames from 'classnames';
-import {
- EmbeddableChildPanel,
- EmbeddablePhaseEvent,
- ViewMode,
-} from '@kbn/embeddable-plugin/public';
+import { EmbeddablePhaseEvent, EmbeddablePanel, ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardPanelState } from '../../../../common';
import { pluginServices } from '../../../services/plugin_services';
@@ -52,9 +48,6 @@ const Item = React.forwardRef(
},
ref
) => {
- const {
- embeddable: { EmbeddablePanel: PanelComponent },
- } = pluginServices.getServices();
const container = useDashboardContainer();
const scrollToPanelId = container.select((state) => state.componentState.scrollToPanelId);
const highlightPanelId = container.select((state) => state.componentState.highlightPanelId);
@@ -90,13 +83,13 @@ const Item = React.forwardRef(
>
{isRenderable ? (
<>
- container.untilEmbeddableLoaded(id)}
/>
{children}
>
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts
index c708937e3d56..d1ae2de6e10c 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts
@@ -6,41 +6,14 @@
* Side Public License, v 1.
*/
-import { HttpStart } from '@kbn/core/public';
import { isErrorEmbeddable, openAddPanelFlyout } from '@kbn/embeddable-plugin/public';
-import { getSavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public';
-
-import { pluginServices } from '../../../services/plugin_services';
import { DashboardContainer } from '../dashboard_container';
export function addFromLibrary(this: DashboardContainer) {
- const {
- overlays,
- notifications,
- usageCollection,
- settings: { uiSettings, theme },
- embeddable: { getEmbeddableFactories, getEmbeddableFactory },
- http,
- savedObjectsManagement,
- savedObjectsTagging,
- } = pluginServices.getServices();
-
if (isErrorEmbeddable(this)) return;
this.openOverlay(
openAddPanelFlyout({
- SavedObjectFinder: getSavedObjectFinder(
- uiSettings,
- http as HttpStart,
- savedObjectsManagement,
- savedObjectsTagging.api
- ),
- reportUiCounter: usageCollection.reportUiCounter,
- getAllFactories: getEmbeddableFactories,
- getFactory: getEmbeddableFactory,
- embeddable: this,
- notifications,
- overlays,
- theme,
+ container: this,
onAddPanel: (id: string) => {
this.setScrollToPanelId(id);
this.setHighlightPanelId(id);
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
index b22c79192657..67bb482b4567 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
@@ -7,47 +7,40 @@
*/
import React from 'react';
-import { mount } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { mount, ReactWrapper } from 'enzyme';
-import type { TimeRange } from '@kbn/es-query';
-import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
-import { findTestSubject, nextTick } from '@kbn/test-jest-helpers';
-import { I18nProvider } from '@kbn/i18n-react';
import {
- CONTEXT_MENU_TRIGGER,
+ ViewMode,
EmbeddablePanel,
isErrorEmbeddable,
- ViewMode,
+ CONTEXT_MENU_TRIGGER,
} from '@kbn/embeddable-plugin/public';
import {
+ EMPTY_EMBEDDABLE,
ContactCardEmbeddable,
- ContactCardEmbeddableFactory,
+ CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
- CONTACT_CARD_EMBEDDABLE,
- EMPTY_EMBEDDABLE,
+ ContactCardEmbeddableFactory,
} from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
-import { applicationServiceMock, coreMock } from '@kbn/core/public/mocks';
+import { I18nProvider } from '@kbn/i18n-react';
+import type { TimeRange } from '@kbn/es-query';
+import { findTestSubject, nextTick } from '@kbn/test-jest-helpers';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
+import { setStubKibanaServices } from '@kbn/embeddable-plugin/public/mocks';
+import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
import { createEditModeActionDefinition } from '@kbn/embeddable-plugin/public/lib/test_samples';
-import { buildMockDashboard, getSampleDashboardInput, getSampleDashboardPanel } from '../../mocks';
-import { pluginServices } from '../../services/plugin_services';
-import { ApplicationStart } from '@kbn/core-application-browser';
import { DashboardContainer } from './dashboard_container';
-
-const theme = coreMock.createStart().theme;
-let application: ApplicationStart | undefined;
+import { pluginServices } from '../../services/plugin_services';
+import { buildMockDashboard, getSampleDashboardInput, getSampleDashboardPanel } from '../../mocks';
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
.fn()
.mockReturnValue(embeddableFactory);
-beforeEach(() => {
- application = applicationServiceMock.createStartContract();
-});
-
test('DashboardContainer initializes embeddables', (done) => {
const container = buildMockDashboard({
panels: {
@@ -191,6 +184,8 @@ test('searchSessionId propagates to children', async () => {
});
test('DashboardContainer in edit mode shows edit mode actions', async () => {
+ // mock embeddable dependencies so that the embeddable panel renders
+ setStubKibanaServices();
const uiActionsSetup = uiActionsPluginMock.createSetupContract();
const editModeAction = createEditModeActionDefinition();
@@ -207,29 +202,26 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
firstName: 'Bob',
});
- const DashboardServicesProvider = pluginServices.getContextProvider();
-
- const component = mount(
-
-
- Promise.resolve([])}
- getAllEmbeddableFactories={(() => []) as any}
- getEmbeddableFactory={(() => null) as any}
- notifications={{} as any}
- application={application}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
-
- );
+ let wrapper: ReactWrapper;
+ await act(async () => {
+ wrapper = await mount(
+
+
+
+ );
+ });
+ const component = wrapper!;
+ await component.update();
+ await nextTick();
const button = findTestSubject(component, 'embeddablePanelToggleMenuIcon');
expect(button.length).toBe(1);
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ act(() => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ });
+ await nextTick();
+ await component.update();
expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
@@ -237,16 +229,26 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
expect(editAction.length).toBe(0);
- container.updateInput({ viewMode: ViewMode.EDIT });
- await nextTick();
- component.update();
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ act(() => {
+ container.updateInput({ viewMode: ViewMode.EDIT });
+ });
+ await nextTick();
+ await component.update();
+
+ act(() => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ });
await nextTick();
component.update();
+
expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(0);
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+
+ act(() => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ });
await nextTick();
component.update();
+
expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(1);
await nextTick();
diff --git a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_functions.ts b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_functions.ts
index fe8e18528e2c..5c9e1854fb10 100644
--- a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_functions.ts
+++ b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_functions.ts
@@ -77,9 +77,9 @@ export const isKeyEqual = (
*/
export const unsavedChangesDiffingFunctions: DashboardDiffFunctions = {
panels: async ({ currentValue, lastValue, container }) => {
- if (!getPanelLayoutsAreEqual(currentValue, lastValue)) return false;
+ if (!getPanelLayoutsAreEqual(currentValue ?? {}, lastValue ?? {})) return false;
- const explicitInputComparePromises = Object.values(currentValue).map(
+ const explicitInputComparePromises = Object.values(currentValue ?? {}).map(
(panel) =>
new Promise((resolve, reject) => {
const embeddableId = panel.explicitInput.id;
@@ -111,8 +111,8 @@ export const unsavedChangesDiffingFunctions: DashboardDiffFunctions = {
// exclude pinned filters from comparision because pinned filters are not part of application state
filters: ({ currentValue, lastValue }) =>
compareFilters(
- currentValue.filter((f) => !isFilterPinned(f)),
- lastValue.filter((f) => !isFilterPinned(f)),
+ (currentValue ?? []).filter((f) => !isFilterPinned(f)),
+ (lastValue ?? []).filter((f) => !isFilterPinned(f)),
COMPARE_ALL_OPTIONS
),
diff --git a/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts b/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts
index a1a6b2973664..655dda0891a6 100644
--- a/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts
+++ b/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts
@@ -20,7 +20,6 @@ export const embeddableServiceFactory: EmbeddableServiceFactory = () => {
getEmbeddableFactory: pluginMock.getEmbeddableFactory,
getStateTransfer: pluginMock.getStateTransfer,
getAllMigrations: pluginMock.getAllMigrations,
- EmbeddablePanel: pluginMock.EmbeddablePanel,
telemetry: pluginMock.telemetry,
extract: pluginMock.extract,
inject: pluginMock.inject,
diff --git a/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts b/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts
index c796bbde2d7d..4c86feccc925 100644
--- a/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts
+++ b/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts
@@ -24,7 +24,6 @@ export const embeddableServiceFactory: EmbeddableServiceFactory = ({ startPlugin
'getEmbeddableFactory',
'getEmbeddableFactories',
'getStateTransfer',
- 'EmbeddablePanel',
'getAllMigrations',
'telemetry',
'extract',
diff --git a/src/plugins/dashboard/public/services/embeddable/types.ts b/src/plugins/dashboard/public/services/embeddable/types.ts
index 40d0ae02bfa7..182c855a146b 100644
--- a/src/plugins/dashboard/public/services/embeddable/types.ts
+++ b/src/plugins/dashboard/public/services/embeddable/types.ts
@@ -14,7 +14,6 @@ export type DashboardEmbeddableService = Pick<
| 'getEmbeddableFactory'
| 'getAllMigrations'
| 'getStateTransfer'
- | 'EmbeddablePanel'
| 'telemetry'
| 'extract'
| 'inject'
diff --git a/src/plugins/embeddable/README.md b/src/plugins/embeddable/README.md
index e7cecad32a56..ab931e559670 100644
--- a/src/plugins/embeddable/README.md
+++ b/src/plugins/embeddable/README.md
@@ -470,7 +470,6 @@ This can ne achieved in two ways by implementing one of the following:
The plugin provides a set of ready-to-use React components that abstract rendering of an embeddable behind a React component:
- `EmbeddablePanel` provides a way to render an embeddable inside a rectangular panel. This also provides error handling and a basic user interface over some of the embeddable properties.
-- `EmbeddableChildPanel` is a higher-order component for the `EmbeddablePanel` that provides a way to render that inside a container.
- `EmbeddableRoot` is the most straightforward wrapper performing rendering of an embeddable.
- `EmbeddableRenderer` is a helper component to render an embeddable or an embeddable factory.
diff --git a/src/plugins/embeddable/jest.config.js b/src/plugins/embeddable/jest.config.js
index e8731b190837..e21fdae6b977 100644
--- a/src/plugins/embeddable/jest.config.js
+++ b/src/plugins/embeddable/jest.config.js
@@ -12,5 +12,6 @@ module.exports = {
roots: ['/src/plugins/embeddable'],
coverageDirectory: '/target/kibana-coverage/jest/src/plugins/embeddable',
coverageReporters: ['text', 'html'],
+ setupFiles: ['/src/plugins/embeddable/jest_setup.ts'],
collectCoverageFrom: ['/src/plugins/embeddable/{common,public,server}/**/*.{ts,tsx}'],
};
diff --git a/src/plugins/embeddable/public/lib/panel/index.ts b/src/plugins/embeddable/jest_setup.ts
similarity index 81%
rename from src/plugins/embeddable/public/lib/panel/index.ts
rename to src/plugins/embeddable/jest_setup.ts
index b5066d27c65a..443fa541e9f2 100644
--- a/src/plugins/embeddable/public/lib/panel/index.ts
+++ b/src/plugins/embeddable/jest_setup.ts
@@ -6,5 +6,5 @@
* Side Public License, v 1.
*/
-export * from './embeddable_panel';
-export * from './panel_header';
+import { setStubKibanaServices } from './public/mocks';
+setStubKibanaServices();
diff --git a/src/plugins/embeddable/kibana.jsonc b/src/plugins/embeddable/kibana.jsonc
index 3b57903fc30c..8de281223e44 100644
--- a/src/plugins/embeddable/kibana.jsonc
+++ b/src/plugins/embeddable/kibana.jsonc
@@ -14,14 +14,8 @@
"savedObjectsFinder",
"savedObjectsManagement"
],
- "optionalPlugins": ["savedObjectsTaggingOss"],
- "requiredBundles": [
- "savedObjects",
- "kibanaReact",
- "kibanaUtils"
- ],
- "extraPublicDirs": [
- "common"
- ]
+ "optionalPlugins": ["savedObjectsTaggingOss", "usageCollection"],
+ "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"],
+ "extraPublicDirs": ["common"]
}
}
diff --git a/src/plugins/embeddable/public/__stories__/embeddable_panel.stories.tsx b/src/plugins/embeddable/public/__stories__/embeddable_panel.stories.tsx
index c7fba909068d..f8fec308eb68 100644
--- a/src/plugins/embeddable/public/__stories__/embeddable_panel.stories.tsx
+++ b/src/plugins/embeddable/public/__stories__/embeddable_panel.stories.tsx
@@ -113,7 +113,6 @@ const HelloWorldEmbeddablePanel = forwardRef<
getActions={getActions}
hideHeader={hideHeader}
showShadow={showShadow}
- theme={{ theme$ }}
/>
);
}
diff --git a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.test.tsx
new file mode 100644
index 000000000000..d1f0db8dd49b
--- /dev/null
+++ b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.test.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import * as React from 'react';
+import { EuiFlyout } from '@elastic/eui';
+import { coreMock } from '@kbn/core/public/mocks';
+import { mountWithIntl as mount } from '@kbn/test-jest-helpers';
+import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
+
+import { AddPanelFlyout } from './add_panel_flyout';
+import { core, embeddableStart, usageCollection } from '../kibana_services';
+import { HelloWorldContainer, ContactCardEmbeddableFactory } from '../lib/test_samples';
+
+// 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;
+ }) => (
+
+ )
+ ),
+ };
+});
+
+describe('add panel flyout', () => {
+ let container: HelloWorldContainer;
+
+ beforeEach(() => {
+ const { overlays } = coreMock.createStart();
+ const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
+ (() => null) as any,
+ overlays
+ );
+
+ embeddableStart.getEmbeddableFactories = jest
+ .fn()
+ .mockReturnValue([contactCardEmbeddableFactory]);
+
+ container = new HelloWorldContainer(
+ {
+ id: '1',
+ panels: {},
+ },
+ {
+ getEmbeddableFactory: embeddableStart.getEmbeddableFactory,
+ }
+ );
+ container.addNewEmbeddable = jest.fn();
+ });
+
+ test('add panel flyout renders SavedObjectFinder', async () => {
+ const component = mount();
+
+ // https://github.com/elastic/kibana/issues/64789
+ expect(component.exists(EuiFlyout)).toBe(false);
+ expect(component.find('#soFinderDummyButton').length).toBe(1);
+ });
+
+ test('add panel adds embeddable to container', async () => {
+ const component = mount();
+
+ expect(Object.values(container.getInput().panels).length).toBe(0);
+ component.find('#soFinderDummyButton').simulate('click');
+ // flush promises
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(container.addNewEmbeddable).toHaveBeenCalled();
+ });
+
+ test('shows a success toast on add', async () => {
+ const component = mount();
+ component.find('#soFinderDummyButton').simulate('click');
+ // 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', async () => {
+ const component = mount();
+
+ expect(Object.values(container.getInput().panels).length).toBe(0);
+ component.find('#soFinderDummyButton').simulate('click');
+ // flush promises
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
+ 'HELLO_WORLD_CONTAINER',
+ 'click',
+ 'CONTACT_CARD_EMBEDDABLE:add'
+ );
+ });
+});
diff --git a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx
new file mode 100644
index 000000000000..82ee19cfaa2c
--- /dev/null
+++ b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx
@@ -0,0 +1,143 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useCallback, useMemo } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { Toast } from '@kbn/core/public';
+import { METRIC_TYPE } from '@kbn/analytics';
+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 {
+ core,
+ embeddableStart,
+ usageCollection,
+ savedObjectsTaggingOss,
+ savedObjectsManagement,
+} from '../kibana_services';
+import {
+ IContainer,
+ EmbeddableFactory,
+ SavedObjectEmbeddableInput,
+ EmbeddableFactoryNotFoundError,
+} from '../lib';
+
+type FactoryMap = { [key: string]: EmbeddableFactory };
+
+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 = (
+ parentType: string,
+ factory: EmbeddableFactory,
+ savedObject: SavedObjectCommon
+) => {
+ const type = factory.savedObjectMetaData?.getSavedObjectSubType
+ ? factory.savedObjectMetaData.getSavedObjectSubType(savedObject)
+ : factory.type;
+
+ usageCollection?.reportUiCounter?.(parentType, METRIC_TYPE.CLICK, `${type}:add`);
+};
+
+export const AddPanelFlyout = ({
+ container,
+ onAddPanel,
+}: {
+ container: IContainer;
+ onAddPanel?: (id: string) => void;
+}) => {
+ const factoriesBySavedObjectType: FactoryMap = useMemo(() => {
+ return [...embeddableStart.getEmbeddableFactories()]
+ .filter((embeddableFactory) => Boolean(embeddableFactory.savedObjectMetaData?.type))
+ .reduce((acc, factory) => {
+ acc[factory.savedObjectMetaData!.type] = factory;
+ return acc;
+ }, {} as FactoryMap);
+ }, []);
+
+ const metaData = useMemo(
+ () =>
+ Object.values(factoriesBySavedObjectType)
+ .filter(
+ (embeddableFactory) =>
+ Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
+ )
+ .map(({ savedObjectMetaData }) => savedObjectMetaData as SavedObjectMetaData),
+ [factoriesBySavedObjectType]
+ );
+
+ const onChoose: SavedObjectFinderProps['onChoose'] = useCallback(
+ async (
+ id: SavedObjectCommon['id'],
+ type: SavedObjectCommon['type'],
+ name: string,
+ savedObject: SavedObjectCommon
+ ) => {
+ const factoryForSavedObjectType = factoriesBySavedObjectType[type];
+ if (!factoryForSavedObjectType) {
+ throw new EmbeddableFactoryNotFoundError(type);
+ }
+
+ const embeddable = await container.addNewEmbeddable(
+ factoryForSavedObjectType.type,
+ { savedObjectId: id }
+ );
+ onAddPanel?.(embeddable.id);
+
+ showSuccessToast(name);
+ runAddTelemetry(container.type, factoryForSavedObjectType, savedObject);
+ },
+ [container, factoriesBySavedObjectType, onAddPanel]
+ );
+
+ return (
+ <>
+
+
+
+ {i18n.translate('embeddableApi.addPanel.Title', { defaultMessage: 'Add from library' })}
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx
new file mode 100644
index 000000000000..d02661d25d6f
--- /dev/null
+++ b/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { Suspense } from 'react';
+
+import { OverlayRef } from '@kbn/core/public';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+
+import { IContainer } from '../lib';
+import { core } from '../kibana_services';
+
+const LazyAddPanelFlyout = React.lazy(async () => {
+ const module = await import('./add_panel_flyout');
+ return { default: module.AddPanelFlyout };
+});
+
+export const openAddPanelFlyout = ({
+ container,
+ onAddPanel,
+}: {
+ container: IContainer;
+ onAddPanel?: (id: string) => void;
+}): OverlayRef => {
+ const flyoutSession = core.overlays.openFlyout(
+ toMountPoint(
+ }>
+
+ ,
+ { theme$: core.theme.theme$ }
+ ),
+ {
+ 'data-test-subj': 'dashboardAddPanel',
+ ownFocus: true,
+ }
+ );
+ return flyoutSession;
+};
diff --git a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx b/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx
deleted file mode 100644
index 0ba7d8b2b30a..000000000000
--- a/src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx
+++ /dev/null
@@ -1,50 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { action } from '@storybook/addon-actions';
-import * as React from 'react';
-import { PanelOptionsMenu } from '..';
-
-export default {
- title: 'components/PanelOptionsMenu',
- component: PanelOptionsMenu,
- argTypes: {
- isViewMode: {
- control: { type: 'boolean' },
- },
- },
- decorators: [
- (Story: React.ComponentType) => (
-
-
-
- ),
- ],
-};
-
-export function Default({ isViewMode }: React.ComponentProps) {
- const euiContextDescriptors = {
- id: 'mainMenu',
- title: 'Options',
- items: [
- {
- name: 'Inspect',
- icon: 'inspect',
- onClick: action('onClick(inspect)'),
- },
- {
- name: 'Full screen',
- icon: 'expand',
- onClick: action('onClick(expand)'),
- },
- ],
- };
-
- return ;
-}
-Default.args = { isViewMode: false } as React.ComponentProps;
diff --git a/src/plugins/embeddable/public/components/panel_options_menu/index.tsx b/src/plugins/embeddable/public/components/panel_options_menu/index.tsx
deleted file mode 100644
index 59b95366ed64..000000000000
--- a/src/plugins/embeddable/public/components/panel_options_menu/index.tsx
+++ /dev/null
@@ -1,79 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import React, { useState, useEffect } from 'react';
-import {
- EuiButtonIcon,
- EuiContextMenu,
- EuiContextMenuPanelDescriptor,
- EuiPopover,
-} from '@elastic/eui';
-
-export interface PanelOptionsMenuProps {
- panelDescriptor?: EuiContextMenuPanelDescriptor;
- close?: boolean;
- isViewMode?: boolean;
- title?: string;
-}
-
-export const PanelOptionsMenu: React.FC = ({
- panelDescriptor,
- close,
- isViewMode,
- title,
-}) => {
- const [open, setOpen] = useState(false);
- useEffect(() => {
- if (!close) setOpen(false);
- }, [close]);
-
- const handleContextMenuClick = () => {
- setOpen((isOpen) => !isOpen);
- };
-
- const handlePopoverClose = () => {
- setOpen(false);
- };
-
- const enhancedAriaLabel = i18n.translate(
- 'embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel',
- {
- defaultMessage: 'Panel options for {title}',
- values: { title },
- }
- );
- const ariaLabelWithoutTitle = i18n.translate(
- 'embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel',
- { defaultMessage: 'Panel options' }
- );
-
- const button = (
-
- );
-
- return (
-
-
-
- );
-};
diff --git a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss b/src/plugins/embeddable/public/embeddable_panel/_embeddable_panel.scss
similarity index 98%
rename from src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
rename to src/plugins/embeddable/public/embeddable_panel/_embeddable_panel.scss
index c63831067399..0facf8bc454b 100644
--- a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
+++ b/src/plugins/embeddable/public/embeddable_panel/_embeddable_panel.scss
@@ -112,6 +112,10 @@
}
+.embPanel__optionsMenuPopover-loading {
+ width: $euiSizeS * 32;
+}
+
.embPanel__optionsMenuPopover-notification::after {
position: absolute;
top: 0;
diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_loading_indicator.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_loading_indicator.tsx
new file mode 100644
index 000000000000..e0a4ca5cf5fe
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_loading_indicator.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiLoadingChart, EuiPanel } from '@elastic/eui';
+
+export const EmbeddableLoadingIndicator = ({ showShadow }: { showShadow?: boolean }) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.test.tsx
new file mode 100644
index 000000000000..b8e5accda354
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.test.tsx
@@ -0,0 +1,516 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { ReactWrapper, mount } from 'enzyme';
+import { I18nProvider } from '@kbn/i18n-react';
+import { nextTick } from '@kbn/test-jest-helpers';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { Action, UiActionsStart, ActionInternal, Trigger } from '@kbn/ui-actions-plugin/public';
+
+import {
+ ContactCardEmbeddable,
+ CONTACT_CARD_EMBEDDABLE,
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddableFactory,
+ CONTACT_CARD_EMBEDDABLE_REACT,
+ createEditModeActionDefinition,
+ ContactCardEmbeddableReactFactory,
+ HelloWorldContainer,
+} from '../lib/test_samples';
+import { EuiBadge } from '@elastic/eui';
+import { embeddablePluginMock } from '../mocks';
+import { EmbeddablePanel } from './embeddable_panel';
+import { core, inspector } from '../kibana_services';
+import { CONTEXT_MENU_TRIGGER, ViewMode } from '..';
+import { UnwrappedEmbeddablePanelProps } from './types';
+
+const actionRegistry = new Map();
+const triggerRegistry = new Map();
+
+const { setup, doStart } = embeddablePluginMock.createInstance();
+
+const editModeAction = createEditModeActionDefinition();
+const trigger: Trigger = {
+ id: CONTEXT_MENU_TRIGGER,
+};
+const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
+const embeddableReactFactory = new ContactCardEmbeddableReactFactory(
+ (() => null) as any,
+ {} as any
+);
+
+actionRegistry.set(editModeAction.id, new ActionInternal(editModeAction));
+triggerRegistry.set(trigger.id, trigger);
+setup.registerEmbeddableFactory(embeddableFactory.type, embeddableFactory);
+setup.registerEmbeddableFactory(embeddableReactFactory.type, embeddableReactFactory);
+
+const start = doStart();
+const getEmbeddableFactory = start.getEmbeddableFactory;
+
+const renderEmbeddableInPanel = async (
+ props: UnwrappedEmbeddablePanelProps
+): Promise => {
+ let wrapper: ReactWrapper;
+ await act(async () => {
+ wrapper = mount(
+
+
+
+ );
+ });
+ return wrapper!;
+};
+
+const setupContainerAndEmbeddable = async (viewMode?: ViewMode, hidePanelTitles?: boolean) => {
+ const container = new HelloWorldContainer(
+ { id: '123', panels: {}, viewMode: viewMode ?? ViewMode.VIEW, hidePanelTitles },
+ {
+ getEmbeddableFactory,
+ } as any
+ );
+
+ const embeddable = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, {
+ firstName: 'Jack',
+ lastName: 'Orange',
+ });
+
+ return { container, embeddable };
+};
+
+const renderInEditModeAndOpenContextMenu = async (
+ embeddableInputs: any,
+ getActions: UiActionsStart['getTriggerCompatibleActions'] = () => Promise.resolve([])
+) => {
+ const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
+ getEmbeddableFactory,
+ } as any);
+
+ const embeddable = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, embeddableInputs);
+
+ let component: ReactWrapper;
+ await act(async () => {
+ component = mount(
+
+
+
+ );
+ });
+
+ findTestSubject(component!, 'embeddablePanelToggleMenuIcon').simulate('click');
+ await nextTick();
+ component!.update();
+
+ return { component: component! };
+};
+
+describe('Error states', () => {
+ let component: ReactWrapper;
+ let embeddable: ContactCardEmbeddable;
+
+ beforeEach(async () => {
+ const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
+ getEmbeddableFactory,
+ } as any);
+
+ embeddable = (await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE, {})) as ContactCardEmbeddable;
+
+ await act(async () => {
+ component = mount(
+
+
+
+ );
+ });
+
+ jest.spyOn(embeddable, 'catchError');
+ });
+
+ test('renders a custom error', () => {
+ act(() => {
+ embeddable.triggerError(new Error('something'));
+ component.update();
+ component.mount();
+ });
+
+ const embeddableError = findTestSubject(component, 'embeddableError');
+
+ expect(embeddable.catchError).toHaveBeenCalledWith(
+ new Error('something'),
+ expect.any(HTMLElement)
+ );
+ expect(embeddableError).toHaveProperty('length', 1);
+ expect(embeddableError.text()).toBe('something');
+ });
+
+ test('renders a custom fatal error', () => {
+ act(() => {
+ embeddable.triggerError(new Error('something'));
+ component.update();
+ component.mount();
+ });
+
+ const embeddableError = findTestSubject(component, 'embeddableError');
+
+ expect(embeddable.catchError).toHaveBeenCalledWith(
+ new Error('something'),
+ expect.any(HTMLElement)
+ );
+ expect(embeddableError).toHaveProperty('length', 1);
+ expect(embeddableError.text()).toBe('something');
+ });
+
+ test('destroys previous error', () => {
+ const { catchError } = embeddable as Required;
+ let destroyError: jest.MockedFunction>;
+
+ (embeddable.catchError as jest.MockedFunction).mockImplementationOnce(
+ (...args) => {
+ destroyError = jest.fn(catchError(...args));
+
+ return destroyError;
+ }
+ );
+ act(() => {
+ embeddable.triggerError(new Error('something'));
+ component.update();
+ component.mount();
+ });
+ act(() => {
+ embeddable.triggerError(new Error('another error'));
+ component.update();
+ component.mount();
+ });
+
+ const embeddableError = findTestSubject(component, 'embeddableError');
+
+ expect(embeddableError).toHaveProperty('length', 1);
+ expect(embeddableError.text()).toBe('another error');
+ expect(destroyError!).toHaveBeenCalledTimes(1);
+ });
+
+ test('renders a default error', async () => {
+ embeddable.catchError = undefined;
+ act(() => {
+ embeddable.triggerError(new Error('something'));
+ component.update();
+ component.mount();
+ });
+
+ const embeddableError = findTestSubject(component, 'embeddableError');
+
+ expect(embeddableError).toHaveProperty('length', 1);
+ expect(embeddableError.children.length).toBeGreaterThan(0);
+ });
+
+ test('renders a React node', () => {
+ (embeddable.catchError as jest.Mock).mockReturnValueOnce(Something
);
+ act(() => {
+ embeddable.triggerError(new Error('something'));
+ component.update();
+ component.mount();
+ });
+
+ const embeddableError = findTestSubject(component, 'embeddableError');
+
+ expect(embeddableError).toHaveProperty('length', 1);
+ expect(embeddableError.text()).toBe('Something');
+ });
+});
+
+test('Render method is called on Embeddable', async () => {
+ const { embeddable } = await setupContainerAndEmbeddable();
+ jest.spyOn(embeddable, 'render');
+ await renderEmbeddableInPanel({ embeddable });
+ expect(embeddable.render).toHaveBeenCalledTimes(1);
+});
+
+test('Actions which are disabled via disabledActions are hidden', async () => {
+ const action = {
+ id: 'FOO',
+ type: 'FOO',
+ getIconType: () => undefined,
+ getDisplayName: () => 'foo',
+ isCompatible: async () => true,
+ execute: async () => {},
+ order: 10,
+ getHref: () => {
+ return Promise.resolve(undefined);
+ },
+ };
+ const getActions = () => Promise.resolve([action]);
+
+ const { component: component1 } = await renderInEditModeAndOpenContextMenu(
+ {
+ firstName: 'Bob',
+ },
+ getActions
+ );
+ const { component: component2 } = await renderInEditModeAndOpenContextMenu(
+ {
+ firstName: 'Bob',
+ disabledActions: ['FOO'],
+ },
+ getActions
+ );
+
+ const fooContextMenuActionItem1 = findTestSubject(component1, 'embeddablePanelAction-FOO');
+ const fooContextMenuActionItem2 = findTestSubject(component2, 'embeddablePanelAction-FOO');
+
+ expect(fooContextMenuActionItem1.length).toBe(1);
+ expect(fooContextMenuActionItem2.length).toBe(0);
+});
+
+test('Badges which are disabled via disabledActions are hidden', async () => {
+ const action = {
+ id: 'BAR',
+ type: 'BAR',
+ getIconType: () => undefined,
+ getDisplayName: () => 'bar',
+ isCompatible: async () => true,
+ execute: async () => {},
+ order: 10,
+ getHref: () => {
+ return Promise.resolve(undefined);
+ },
+ };
+ const getActions = () => Promise.resolve([action]);
+
+ const { component: component1 } = await renderInEditModeAndOpenContextMenu(
+ {
+ firstName: 'Bob',
+ },
+ getActions
+ );
+ const { component: component2 } = await renderInEditModeAndOpenContextMenu(
+ {
+ firstName: 'Bob',
+ disabledActions: ['BAR'],
+ },
+ getActions
+ );
+
+ expect(component1.find(EuiBadge).length).toBe(1);
+ expect(component2.find(EuiBadge).length).toBe(0);
+});
+
+test('Edit mode actions are hidden if parent is in view mode', async () => {
+ const { embeddable } = await setupContainerAndEmbeddable();
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ await act(async () => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ await nextTick();
+ component.update();
+ });
+ expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
+ await nextTick();
+ component.update();
+ expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0);
+});
+
+test('Edit mode actions are shown in edit mode', async () => {
+ const { container, embeddable } = await setupContainerAndEmbeddable();
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ const button = findTestSubject(component, 'embeddablePanelToggleMenuIcon');
+
+ expect(button.length).toBe(1);
+ await act(async () => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ await nextTick();
+ component.update();
+ });
+ expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
+ await nextTick();
+ act(() => {
+ component.update();
+ });
+ expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0);
+
+ await act(async () => {
+ container.updateInput({ viewMode: ViewMode.EDIT });
+ await nextTick();
+ component.update();
+ });
+
+ // Need to close and re-open to refresh. It doesn't update automatically.
+ await act(async () => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ await nextTick();
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ await nextTick();
+ component.update();
+ });
+ expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(1);
+
+ await act(async () => {
+ container.updateInput({ viewMode: ViewMode.VIEW });
+ await nextTick();
+ component.update();
+ });
+
+ // TODO: Fix this.
+ // const action = findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`);
+ // expect(action.length).toBe(1);
+});
+
+test('Panel title customize link does not exist in view mode', async () => {
+ const { embeddable } = await setupContainerAndEmbeddable(ViewMode.VIEW, false);
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
+ expect(titleLink.length).toBe(0);
+});
+
+test('Runs customize panel action on title click when in edit mode', async () => {
+ // spy on core openFlyout to check that the flyout is opened correctly.
+ core.overlays.openFlyout = jest.fn();
+
+ const { embeddable } = await setupContainerAndEmbeddable(ViewMode.EDIT, false);
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
+ expect(titleLink.length).toBe(1);
+ act(() => {
+ titleLink.simulate('click');
+ });
+ await nextTick();
+ expect(core.overlays.openFlyout).toHaveBeenCalledTimes(1);
+ expect(core.overlays.openFlyout).toHaveBeenCalledWith(
+ expect.any(Function),
+ expect.objectContaining({ 'data-test-subj': 'customizePanel' })
+ );
+});
+
+test('Updates when hidePanelTitles is toggled', async () => {
+ const { container, embeddable } = await setupContainerAndEmbeddable(ViewMode.VIEW, false);
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ await component.update();
+ let title = findTestSubject(component, `embeddablePanelHeading-HelloJackOrange`);
+ expect(title.length).toBe(1);
+
+ await act(async () => {
+ await container.updateInput({ hidePanelTitles: true });
+ });
+
+ await nextTick();
+ await component.update();
+ title = findTestSubject(component, `embeddablePanelHeading-HelloJackOrange`);
+ expect(title.length).toBe(0);
+
+ await act(async () => {
+ await container.updateInput({ hidePanelTitles: false });
+ await nextTick();
+ component.update();
+ });
+
+ title = findTestSubject(component, `embeddablePanelHeading-HelloJackOrange`);
+ expect(title.length).toBe(1);
+});
+
+test('Respects options from SelfStyledEmbeddable', async () => {
+ const { container, embeddable } = await setupContainerAndEmbeddable(ViewMode.VIEW, false);
+
+ const selfStyledEmbeddable = embeddablePluginMock.mockSelfStyledEmbeddable(embeddable, {
+ hideTitle: true,
+ });
+
+ // make sure the title is being hidden because of the self styling, not the container
+ container.updateInput({ hidePanelTitles: false });
+
+ const component = await renderEmbeddableInPanel({ embeddable: selfStyledEmbeddable });
+
+ const title = findTestSubject(component, `embeddablePanelHeading-HelloJackOrange`);
+ expect(title.length).toBe(0);
+});
+
+test('Does not hide header when parent hide header option is false', async () => {
+ const { embeddable } = await setupContainerAndEmbeddable(ViewMode.VIEW, false);
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ const title = findTestSubject(component, `embeddablePanelHeading-HelloJackOrange`);
+ expect(title.length).toBe(1);
+});
+
+test('Hides title when parent hide header option is true', async () => {
+ const { embeddable } = await setupContainerAndEmbeddable(ViewMode.VIEW, true);
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ const title = findTestSubject(component, `embeddablePanelHeading-HelloJackOrange`);
+ expect(title.length).toBe(0);
+});
+
+test('Should work in minimal way rendering only the inspector action', async () => {
+ inspector.isAvailable = jest.fn(() => true);
+
+ const { embeddable } = await setupContainerAndEmbeddable(ViewMode.VIEW, true);
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ await act(async () => {
+ findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
+ await nextTick();
+ component.update();
+ });
+
+ expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
+ await act(async () => {
+ await nextTick();
+ component.update();
+ });
+ expect(findTestSubject(component, `embeddablePanelAction-openInspector`).length).toBe(1);
+ const action = findTestSubject(component, `embeddablePanelAction-ACTION_CUSTOMIZE_PANEL`);
+ expect(action.length).toBe(0);
+});
+
+test('Renders an embeddable returning a React node', async () => {
+ const container = new HelloWorldContainer(
+ { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
+ { getEmbeddableFactory } as any
+ );
+
+ const embeddable = await container.addNewEmbeddable<
+ ContactCardEmbeddableInput,
+ ContactCardEmbeddableOutput,
+ ContactCardEmbeddable
+ >(CONTACT_CARD_EMBEDDABLE_REACT, {
+ firstName: 'Bran',
+ lastName: 'Stark',
+ });
+
+ const component = await renderEmbeddableInPanel({ embeddable });
+
+ expect(component.find('.embPanel__titleText').text()).toBe('Hello Bran Stark');
+});
diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
new file mode 100644
index 000000000000..a7032f894f4a
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
@@ -0,0 +1,221 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { isNil } from 'lodash';
+import classNames from 'classnames';
+import { distinct, map } from 'rxjs';
+import React, { ReactNode, useEffect, useMemo, useState } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiPanel, htmlIdGenerator } from '@elastic/eui';
+
+import { isPromise } from '@kbn/std';
+import { UI_SETTINGS } from '@kbn/data-plugin/common';
+
+import {
+ EditPanelAction,
+ RemovePanelAction,
+ InspectPanelAction,
+ CustomizePanelAction,
+} from './panel_actions';
+import {
+ EmbeddablePhase,
+ EmbeddablePhaseEvent,
+ PanelUniversalActions,
+ UnwrappedEmbeddablePanelProps,
+} from './types';
+import {
+ useSelectFromEmbeddableInput,
+ useSelectFromEmbeddableOutput,
+} from './use_select_from_embeddable';
+import { EmbeddablePanelError } from './embeddable_panel_error';
+import { core, embeddableStart, inspector } from '../kibana_services';
+import { ViewMode, EmbeddableErrorHandler, EmbeddableOutput } from '../lib';
+import { EmbeddablePanelHeader } from './panel_header/embeddable_panel_header';
+
+const getEventStatus = (output: EmbeddableOutput): EmbeddablePhase => {
+ if (!isNil(output.error)) {
+ return 'error';
+ } else if (output.rendered === true) {
+ return 'rendered';
+ } else if (output.loading === false) {
+ return 'loaded';
+ } else {
+ return 'loading';
+ }
+};
+
+export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
+ const {
+ hideHeader,
+ showShadow,
+ embeddable,
+ hideInspector,
+ containerContext,
+ onPanelStatusChange,
+ } = panelProps;
+ const [node, setNode] = useState();
+ const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []);
+
+ const headerId = useMemo(() => htmlIdGenerator()(), []);
+ const [outputError, setOutputError] = useState();
+
+ /**
+ * Universal actions are exposed on the context menu for every embeddable, they
+ * bypass the trigger registry.
+ */
+ const universalActions = useMemo(() => {
+ const commonlyUsedRanges = core.uiSettings.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES);
+ const dateFormat = core.uiSettings.get(UI_SETTINGS.DATE_FORMAT);
+ const stateTransfer = embeddableStart.getStateTransfer();
+
+ const actions: PanelUniversalActions = {
+ customizePanel: new CustomizePanelAction(
+ core.overlays,
+ core.theme,
+ commonlyUsedRanges,
+ dateFormat
+ ),
+ removePanel: new RemovePanelAction(),
+ editPanel: new EditPanelAction(
+ embeddableStart.getEmbeddableFactory,
+ core.application,
+ stateTransfer,
+ containerContext?.getCurrentPath
+ ),
+ };
+ if (!hideInspector) actions.inspectPanel = new InspectPanelAction(inspector);
+ return actions;
+ }, [containerContext?.getCurrentPath, hideInspector]);
+
+ /**
+ * Track panel status changes
+ */
+ useEffect(() => {
+ if (!onPanelStatusChange) return;
+ let loadingStartTime = 0;
+
+ const subscription = embeddable
+ .getOutput$()
+ .pipe(
+ // Map loaded event properties
+ map((output) => {
+ if (output.loading === true) {
+ loadingStartTime = performance.now();
+ }
+ return {
+ id: embeddable.id,
+ status: getEventStatus(output),
+ error: output.error,
+ };
+ }),
+ // Dedupe
+ distinct((output) => loadingStartTime + output.id + output.status + !!output.error),
+ // Map loaded event properties
+ map((output): EmbeddablePhaseEvent => {
+ return {
+ ...output,
+ timeToEvent: performance.now() - loadingStartTime,
+ };
+ })
+ )
+ .subscribe((statusOutput) => {
+ onPanelStatusChange(statusOutput);
+ });
+ return () => subscription?.unsubscribe();
+
+ // Panel status change subscription should only be run on mount.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ /**
+ * Select state from the embeddable
+ */
+ const loading = useSelectFromEmbeddableOutput('loading', embeddable);
+ const viewMode = useSelectFromEmbeddableInput('viewMode', embeddable);
+
+ /**
+ * Render embeddable into ref, set up error subscription
+ */
+ useEffect(() => {
+ if (!embeddableRoot.current) return;
+ const nextNode = embeddable.render(embeddableRoot.current) ?? undefined;
+ if (isPromise(nextNode)) {
+ nextNode.then((resolved) => setNode(resolved));
+ } else {
+ setNode(nextNode);
+ }
+ const errorSubscription = embeddable.getOutput$().subscribe({
+ next: (output) => {
+ setOutputError(output.error);
+ },
+ error: (error) => setOutputError(error),
+ });
+ return () => {
+ embeddable?.destroy();
+ errorSubscription?.unsubscribe();
+ };
+ }, [embeddable, embeddableRoot]);
+
+ const classes = useMemo(
+ () =>
+ classNames('embPanel', {
+ 'embPanel--editing': viewMode !== ViewMode.VIEW,
+ 'embPanel--loading': loading,
+ }),
+ [viewMode, loading]
+ );
+
+ const contentAttrs = useMemo(() => {
+ const attrs: { [key: string]: boolean } = {};
+ if (loading) attrs['data-loading'] = true;
+ if (outputError) attrs['data-error'] = true;
+ return attrs;
+ }, [loading, outputError]);
+
+ return (
+
+ {!hideHeader && (
+
+ )}
+ {outputError && (
+
+
+
+ {(error) => (
+
+ )}
+
+
+
+ )}
+
+ {node}
+
+
+ );
+};
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel_error.tsx
similarity index 97%
rename from src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx
rename to src/plugins/embeddable/public/embeddable_panel/embeddable_panel_error.tsx
index b5b9eaa267bd..8d7055b342ce 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel_error.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel_error.tsx
@@ -6,15 +6,17 @@
* Side Public License, v 1.
*/
+import { i18n } from '@kbn/i18n';
+import { distinctUntilChanged, merge, of, switchMap } from 'rxjs';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import { EuiButtonEmpty, EuiEmptyPrompt, EuiText } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { Markdown } from '@kbn/kibana-react-plugin/public';
+
import type { MaybePromise } from '@kbn/utility-types';
+import { Markdown } from '@kbn/kibana-react-plugin/public';
import { ErrorLike } from '@kbn/expressions-plugin/common';
-import { distinctUntilChanged, merge, of, switchMap } from 'rxjs';
-import { EditPanelAction } from '../actions';
-import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../embeddables';
+
+import { EditPanelAction } from './panel_actions';
+import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../lib/embeddables';
interface EmbeddablePanelErrorProps {
editPanelAction?: EditPanelAction;
diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel_strings.ts b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel_strings.ts
new file mode 100644
index 000000000000..8816c9085bd0
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel_strings.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const placeholderTitle = i18n.translate('embeddableApi.panel.placeholderTitle', {
+ defaultMessage: '[No Title]',
+});
+
+export const getAriaLabelForTitle = (title?: string) => {
+ if (title) {
+ return i18n.translate('embeddableApi.panel.enhancedDashboardPanelAriaLabel', {
+ defaultMessage: 'Dashboard panel: {title}',
+ values: { title: title || placeholderTitle },
+ });
+ }
+ return i18n.translate('embeddableApi.panel.dashboardPanelAriaLabel', {
+ defaultMessage: 'Dashboard panel',
+ });
+};
+
+export const getEditTitleAriaLabel = (title?: string) =>
+ i18n.translate('embeddableApi.panel.editTitleAriaLabel', {
+ defaultMessage: 'Click to edit title: {title}',
+ values: { title: title || placeholderTitle },
+ });
+
+export const getContextMenuAriaLabel = (title?: string, index?: number) => {
+ if (title) {
+ return i18n.translate('embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel', {
+ defaultMessage: 'Panel options for {title}',
+ values: { title },
+ });
+ }
+ if (index) {
+ return i18n.translate('embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabelWithIndex', {
+ defaultMessage: 'Options for panel {index}',
+ values: { index },
+ });
+ }
+ return i18n.translate('embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel', {
+ defaultMessage: 'Panel options',
+ });
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/index.tsx b/src/plugins/embeddable/public/embeddable_panel/index.tsx
new file mode 100644
index 000000000000..9e29d49cbfd5
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/index.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+
+import { EmbeddablePanelProps } from './types';
+import { useEmbeddablePanel } from './use_embeddable_panel';
+import { EmbeddableLoadingIndicator } from './embeddable_loading_indicator';
+
+/**
+ * Loads and renders an embeddable.
+ */
+export const EmbeddablePanel = (props: EmbeddablePanelProps) => {
+ const result = useEmbeddablePanel({ embeddable: props.embeddable });
+ if (!result) return ;
+ const { embeddable, ...passThroughProps } = props;
+ return ;
+};
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.test.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/can_inherit_time_range.test.ts
similarity index 93%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.test.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/can_inherit_time_range.test.ts
index 0bec8ab73d91..d55c851dcc5e 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.test.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/can_inherit_time_range.test.ts
@@ -11,8 +11,8 @@ import {
HelloWorldContainer,
TimeRangeContainer,
TimeRangeEmbeddable,
-} from '../../../../test_samples';
-import { HelloWorldEmbeddable } from '../../../../../tests/fixtures';
+} from '../../../lib/test_samples';
+import { HelloWorldEmbeddable } from '../../../tests/fixtures';
test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => {
const embeddable = new TimeRangeEmbeddable(
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/can_inherit_time_range.ts
similarity index 99%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/can_inherit_time_range.ts
index 598a921f3001..139933c8d939 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/can_inherit_time_range.ts
@@ -7,8 +7,9 @@
*/
import type { TimeRange } from '@kbn/es-query';
-import { Embeddable, IContainer, ContainerInput } from '../../../../..';
+
import { TimeRangeInput } from './customize_panel_action';
+import { Embeddable, IContainer, ContainerInput } from '../../..';
interface ContainerTimeRangeInput extends ContainerInput {
timeRange: TimeRange;
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.test.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/custom_time_range_badge.test.ts
similarity index 98%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.test.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/custom_time_range_badge.test.ts
index dc2c02fb4462..36eb8bd1d282 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.test.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/custom_time_range_badge.test.ts
@@ -6,13 +6,14 @@
* Side Public License, v 1.
*/
-import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
+import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
+
import {
TimeRangeEmbeddable,
TimeRangeContainer,
TIME_RANGE_EMBEDDABLE,
-} from '../../../../test_samples/embeddables';
+} from '../../../lib/test_samples/embeddables';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
test(`badge is not compatible with embeddable that inherits from parent`, async () => {
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/custom_time_range_badge.tsx
similarity index 97%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.tsx
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/custom_time_range_badge.tsx
index 93c64122351f..08e864b76b1a 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/custom_time_range_badge.tsx
@@ -7,11 +7,12 @@
*/
import React from 'react';
-import { renderToString } from 'react-dom/server';
import { PrettyDuration } from '@elastic/eui';
+import { renderToString } from 'react-dom/server';
import { Action } from '@kbn/ui-actions-plugin/public';
+
+import { Embeddable } from '../../..';
import { doesInheritTimeRange } from './does_inherit_time_range';
-import { Embeddable } from '../../../../..';
import { TimeRangeInput, hasTimeRange, CustomizePanelAction } from './customize_panel_action';
export const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.test.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_action.test.ts
similarity index 84%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.test.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_action.test.ts
index 0d67a41c7cd1..7e88a6f2037b 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.test.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_action.test.ts
@@ -8,19 +8,19 @@
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
-import { Container, isErrorEmbeddable } from '../../../..';
+import { Container, isErrorEmbeddable } from '../../..';
import { CustomizePanelAction } from './customize_panel_action';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
-} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable';
+} from '../../../lib/test_samples/embeddables/contact_card/contact_card_embeddable';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
-} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
-import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container';
-import { embeddablePluginMock } from '../../../../../mocks';
+} from '../../../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
+import { HelloWorldContainer } from '../../../lib/test_samples/embeddables/hello_world_container';
+import { embeddablePluginMock } from '../../../mocks';
let container: Container;
let embeddable: ContactCardEmbeddable;
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_action.tsx
similarity index 96%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_action.tsx
index 6146199eee24..1dc0a65e16b5 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_action.tsx
@@ -8,19 +8,14 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
+import { TimeRange } from '@kbn/es-query';
+import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { OverlayRef, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
-import { TimeRange } from '@kbn/es-query';
-import { ViewMode } from '../../../../types';
-import {
- IEmbeddable,
- Embeddable,
- EmbeddableInput,
- CommonlyUsedRange,
- EmbeddableOutput,
-} from '../../../..';
+
import { CustomizePanelEditor } from './customize_panel_editor';
+import { ViewMode, CommonlyUsedRange } from '../../../lib/types';
+import { IEmbeddable, Embeddable, EmbeddableInput, EmbeddableOutput } from '../../..';
export const ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_editor.tsx
similarity index 99%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor.tsx
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_editor.tsx
index f51828159f89..2e5eafa9c7c4 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/customize_panel_editor.tsx
@@ -24,13 +24,14 @@ import {
EuiFlexItem,
EuiSuperDatePicker,
} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { TimeRange } from '@kbn/es-query';
+import { FormattedMessage } from '@kbn/i18n-react';
+
import { TimeRangeInput } from './customize_panel_action';
-import { doesInheritTimeRange } from './does_inherit_time_range';
-import { IEmbeddable, Embeddable, CommonlyUsedRange, ViewMode } from '../../../..';
import { canInheritTimeRange } from './can_inherit_time_range';
+import { doesInheritTimeRange } from './does_inherit_time_range';
+import { IEmbeddable, Embeddable, CommonlyUsedRange, ViewMode } from '../../../lib';
type PanelSettings = {
title?: string;
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/does_inherit_time_range.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/does_inherit_time_range.ts
similarity index 99%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/does_inherit_time_range.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/does_inherit_time_range.ts
index 3fa68bd3d582..a14ca031c9fb 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/does_inherit_time_range.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/does_inherit_time_range.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { Embeddable, IContainer, ContainerInput } from '../../../..';
+import { Embeddable, IContainer, ContainerInput } from '../../../lib';
import { TimeRangeInput } from './customize_panel_action';
export function doesInheritTimeRange(embeddable: Embeddable) {
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/index.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/index.ts
similarity index 90%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/index.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/index.ts
index 8321d7179386..adaa8e4cbcc1 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/index.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/customize_panel_action/index.ts
@@ -7,3 +7,4 @@
*/
export * from './customize_panel_action';
+export * from './custom_time_range_badge';
diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
similarity index 94%
rename from src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
index d2fb4e701e4b..a1b1bf4df5ec 100644
--- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
@@ -6,14 +6,15 @@
* Side Public License, v 1.
*/
-import { EditPanelAction } from './edit_panel_action';
-import { Embeddable, EmbeddableInput } from '../embeddables';
-import { ViewMode } from '../types';
-import { ContactCardEmbeddable } from '../test_samples';
-import { embeddablePluginMock } from '../../mocks';
-import { applicationServiceMock } from '@kbn/core/public/mocks';
import { of } from 'rxjs';
+import { ViewMode } from '../../../lib';
+import { EditPanelAction } from './edit_panel_action';
+import { embeddablePluginMock } from '../../../mocks';
+import { applicationServiceMock } from '@kbn/core/public/mocks';
+import { ContactCardEmbeddable } from '../../../lib/test_samples';
+import { Embeddable, EmbeddableInput } from '../../../lib/embeddables';
+
const { doStart } = embeddablePluginMock.createInstance();
const start = doStart();
const getFactory = start.getEmbeddableFactory;
diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
similarity index 96%
rename from src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
index ba59d92cbef6..942830da96e6 100644
--- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
@@ -7,19 +7,21 @@
*/
import { i18n } from '@kbn/i18n';
+import { take } from 'rxjs/operators';
+
import { ApplicationStart } from '@kbn/core/public';
import { Action } from '@kbn/ui-actions-plugin/public';
-import { take } from 'rxjs/operators';
-import { ViewMode } from '../types';
-import { EmbeddableFactoryNotFoundError } from '../errors';
-import { EmbeddableStart } from '../../plugin';
+
import {
+ Container,
IEmbeddable,
+ EmbeddableInput,
EmbeddableEditorState,
EmbeddableStateTransfer,
- EmbeddableInput,
- Container,
-} from '../..';
+} from '../../../lib';
+import { ViewMode } from '../../../lib/types';
+import { EmbeddableStart } from '../../../plugin';
+import { EmbeddableFactoryNotFoundError } from '../../../lib/errors';
export const ACTION_EDIT_PANEL = 'editPanel';
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/index.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/index.ts
new file mode 100644
index 000000000000..e29ee2b9fe34
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/index.ts
@@ -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
+ * 2.0 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 or the Server
+ * Side Public License, v 1.
+ */
+
+export {
+ InspectPanelAction,
+ ACTION_INSPECT_PANEL,
+} from './inspect_panel_action/inspect_panel_action';
+export {
+ CustomizePanelAction,
+ CustomTimeRangeBadge,
+ ACTION_CUSTOMIZE_PANEL,
+} from './customize_panel_action';
+export { EditPanelAction, ACTION_EDIT_PANEL } from './edit_panel_action/edit_panel_action';
+export { RemovePanelAction, REMOVE_PANEL_ACTION } from './remove_panel_action/remove_panel_action';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/inspect_panel_action/inspect_panel_action.test.tsx
similarity index 95%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/inspect_panel_action/inspect_panel_action.test.tsx
index 5c7483588d19..d41cd5986680 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/inspect_panel_action/inspect_panel_action.test.tsx
@@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
-import { InspectPanelAction } from './inspect_panel_action';
+import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
+
import {
FilterableContainer,
FILTERABLE_EMBEDDABLE,
@@ -14,12 +15,12 @@ import {
FilterableEmbeddableInput,
FilterableEmbeddable,
ContactCardEmbeddable,
-} from '../../../test_samples';
-import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
-import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../embeddables';
-import { of } from '../../../../tests/helpers';
-import { embeddablePluginMock } from '../../../../mocks';
-import { EmbeddableStart } from '../../../../plugin';
+} from '../../../lib/test_samples';
+import { of } from '../../../tests/helpers';
+import { EmbeddableStart } from '../../../plugin';
+import { embeddablePluginMock } from '../../../mocks';
+import { InspectPanelAction } from './inspect_panel_action';
+import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../lib/embeddables';
const setupTests = async () => {
const { setup, doStart } = embeddablePluginMock.createInstance();
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/inspect_panel_action/inspect_panel_action.ts
similarity index 97%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/inspect_panel_action/inspect_panel_action.ts
index 08c08fc63e89..ef2dbe941ee5 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/inspect_panel_action/inspect_panel_action.ts
@@ -9,7 +9,8 @@
import { i18n } from '@kbn/i18n';
import { Action } from '@kbn/ui-actions-plugin/public';
import { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
-import { IEmbeddable } from '../../../embeddables';
+
+import { IEmbeddable } from '../../../lib/embeddables';
export const ACTION_INSPECT_PANEL = 'openInspector';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/remove_panel_action/remove_panel_action.test.tsx
similarity index 83%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/remove_panel_action/remove_panel_action.test.tsx
index b04aa4a0bd9a..09b84aa6929e 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/remove_panel_action/remove_panel_action.test.tsx
@@ -7,19 +7,20 @@
*/
import { EmbeddableOutput, isErrorEmbeddable } from '../../..';
-import { RemovePanelAction } from './remove_panel_action';
-import { EmbeddableStart } from '../../../../plugin';
+
import {
MockFilter,
FILTERABLE_EMBEDDABLE,
FilterableEmbeddable,
FilterableEmbeddableInput,
-} from '../../../test_samples/embeddables/filterable_embeddable';
-import { FilterableEmbeddableFactory } from '../../../test_samples/embeddables/filterable_embeddable_factory';
-import { FilterableContainer } from '../../../test_samples/embeddables/filterable_container';
-import { ViewMode } from '../../../types';
-import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable';
-import { embeddablePluginMock } from '../../../../mocks';
+} from '../../../lib/test_samples/embeddables/filterable_embeddable';
+import { ViewMode } from '../../../lib/types';
+import { EmbeddableStart } from '../../../plugin';
+import { embeddablePluginMock } from '../../../mocks';
+import { RemovePanelAction } from './remove_panel_action';
+import { FilterableContainer } from '../../../lib/test_samples/embeddables/filterable_container';
+import { FilterableEmbeddableFactory } from '../../../lib/test_samples/embeddables/filterable_embeddable_factory';
+import { ContactCardEmbeddable } from '../../../lib/test_samples/embeddables/contact_card/contact_card_embeddable';
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/remove_panel_action/remove_panel_action.ts
similarity index 90%
rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts
rename to src/plugins/embeddable/public/embeddable_panel/panel_actions/remove_panel_action/remove_panel_action.ts
index ea8134310097..e9729de908ec 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/remove_panel_action/remove_panel_action.ts
@@ -7,9 +7,10 @@
*/
import { i18n } from '@kbn/i18n';
-import { Action, IncompatibleActionError } from '../../../ui_actions';
-import { ViewMode } from '../../../types';
-import { IEmbeddable } from '../../../embeddables';
+
+import { ViewMode } from '../../../lib/types';
+import { IEmbeddable } from '../../../lib/embeddables';
+import { Action, IncompatibleActionError } from '../../../lib/ui_actions';
export const REMOVE_PANEL_ACTION = 'deletePanel';
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_context_menu.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_context_menu.tsx
new file mode 100644
index 000000000000..5fe6461083cc
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_context_menu.tsx
@@ -0,0 +1,171 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import classNames from 'classnames';
+import React, { useEffect, useMemo, useState } from 'react';
+
+import {
+ EuiButtonIcon,
+ EuiContextMenu,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiContextMenuPanelDescriptor,
+ EuiPopover,
+ EuiSkeletonText,
+} from '@elastic/eui';
+import { Action, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
+
+import { uiActions } from '../../kibana_services';
+import { EmbeddablePanelProps, PanelUniversalActions } from '../types';
+import { getContextMenuAriaLabel } from '../embeddable_panel_strings';
+import { useSelectFromEmbeddableInput } from '../use_select_from_embeddable';
+import { IEmbeddable, contextMenuTrigger, CONTEXT_MENU_TRIGGER, ViewMode } from '../..';
+
+const sortByOrderField = (
+ { order: orderA }: { order?: number },
+ { order: orderB }: { order?: number }
+) => (orderB || 0) - (orderA || 0);
+
+const removeById =
+ (disabledActions: string[]) =>
+ ({ id }: { id: string }) =>
+ disabledActions.indexOf(id) === -1;
+
+export const EmbeddablePanelContextMenu = ({
+ index,
+ embeddable,
+ getActions,
+ actionPredicate,
+ universalActions,
+}: {
+ index?: number;
+ embeddable: IEmbeddable;
+ universalActions: PanelUniversalActions;
+ getActions: EmbeddablePanelProps['getActions'];
+ actionPredicate?: (actionId: string) => boolean;
+}) => {
+ const [menuPanelsLoading, setMenuPanelsLoading] = useState(false);
+ const [contextMenuActions, setContextMenuActions] = useState>>([]);
+ const [isContextMenuOpen, setIsContextMenuOpen] = useState(undefined);
+ const [contextMenuPanels, setContextMenuPanels] = useState([]);
+
+ const title = useSelectFromEmbeddableInput('title', embeddable);
+ const viewMode = useSelectFromEmbeddableInput('viewMode', embeddable);
+
+ useEffect(() => {
+ /**
+ * isContextMenuOpen starts as undefined which allows this use effect to run on mount. This
+ * is required so that showNotification is calculated on mount.
+ */
+ if (isContextMenuOpen === false) return;
+
+ setMenuPanelsLoading(true);
+ let canceled = false;
+ (async () => {
+ /**
+ * Build and update all actions
+ */
+ const regularActions = await (async () => {
+ if (getActions) return await getActions(CONTEXT_MENU_TRIGGER, { embeddable });
+ return (
+ (await uiActions.getTriggerCompatibleActions(CONTEXT_MENU_TRIGGER, {
+ embeddable,
+ })) ?? []
+ );
+ })();
+ if (canceled) return;
+ let allActions = regularActions.concat(
+ Object.values(universalActions ?? {}) as Array>
+ );
+
+ const { disabledActions } = embeddable.getInput();
+ if (disabledActions) {
+ const removeDisabledActions = removeById(disabledActions);
+ allActions = allActions.filter(removeDisabledActions);
+ }
+ allActions.sort(sortByOrderField);
+
+ if (actionPredicate) {
+ allActions = allActions.filter(({ id }) => actionPredicate(id));
+ }
+
+ /**
+ * Build context menu panel from actions
+ */
+ const panels = await buildContextMenuForActions({
+ actions: allActions.map((action) => ({
+ action,
+ context: { embeddable },
+ trigger: contextMenuTrigger,
+ })),
+ closeMenu: () => setIsContextMenuOpen(false),
+ });
+ if (canceled) return;
+
+ setMenuPanelsLoading(false);
+ setContextMenuActions(allActions);
+ setContextMenuPanels(panels);
+ })();
+ return () => {
+ canceled = true;
+ };
+ }, [actionPredicate, embeddable, getActions, isContextMenuOpen, universalActions]);
+
+ const showNotification = useMemo(
+ () => contextMenuActions.some((action) => action.showNotification),
+ [contextMenuActions]
+ );
+
+ const contextMenuClasses = classNames({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ embPanel__optionsMenuPopover: true,
+ 'embPanel__optionsMenuPopover-notification': showNotification,
+ });
+
+ const ContextMenuButton = (
+ setIsContextMenuOpen((isOpen) => !isOpen)}
+ iconType={viewMode === ViewMode.VIEW ? 'boxesHorizontal' : 'gear'}
+ />
+ );
+
+ return (
+ setIsContextMenuOpen(false)}
+ data-test-subj={
+ isContextMenuOpen ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'
+ }
+ >
+ {menuPanelsLoading ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_header.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_header.tsx
new file mode 100644
index 000000000000..8d79d143eade
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_header.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import classNames from 'classnames';
+import React, { useMemo } from 'react';
+import { EuiScreenReaderOnly } from '@elastic/eui';
+
+import { isSelfStyledEmbeddable, ViewMode } from '../../lib';
+import { EmbeddablePanelTitle } from './embeddable_panel_title';
+import { getAriaLabelForTitle } from '../embeddable_panel_strings';
+import { useEmbeddablePanelBadges } from './use_embeddable_panel_badges';
+import { useSelectFromEmbeddableInput } from '../use_select_from_embeddable';
+import { EmbeddablePanelContextMenu } from './embeddable_panel_context_menu';
+import { UnwrappedEmbeddablePanelProps, PanelUniversalActions } from '../types';
+
+export const EmbeddablePanelHeader = ({
+ index,
+ headerId,
+ getActions,
+ embeddable,
+ actionPredicate,
+ universalActions,
+ showBadges = true,
+ showNotifications = true,
+}: UnwrappedEmbeddablePanelProps & {
+ headerId: string;
+ universalActions: PanelUniversalActions;
+}) => {
+ const selfStyledEmbeddableOptions = useMemo(
+ () => (isSelfStyledEmbeddable(embeddable) ? embeddable.getSelfStyledOptions() : undefined),
+ [embeddable]
+ );
+
+ const { notificationComponents, badgeComponents } = useEmbeddablePanelBadges(
+ embeddable,
+ getActions
+ );
+
+ const title = embeddable.getTitle();
+ const viewMode = useSelectFromEmbeddableInput('viewMode', embeddable);
+ const description = useSelectFromEmbeddableInput('description', embeddable);
+ const hidePanelTitle = useSelectFromEmbeddableInput('hidePanelTitles', embeddable);
+ const parentHidePanelTitle = useSelectFromEmbeddableInput('hidePanelTitles', embeddable.parent);
+
+ const hideTitle =
+ Boolean(hidePanelTitle) ||
+ Boolean(parentHidePanelTitle) ||
+ Boolean(selfStyledEmbeddableOptions?.hideTitle) ||
+ (viewMode === ViewMode.VIEW && !Boolean(title));
+
+ const showPanelBar =
+ !hideTitle ||
+ description ||
+ viewMode !== ViewMode.VIEW ||
+ (badgeComponents?.length ?? 0) > 0 ||
+ (notificationComponents?.length ?? 0) > 0;
+
+ const ariaLabel = getAriaLabelForTitle(showPanelBar ? title : undefined);
+ const ariaLabelElement = (
+
+
+
+ );
+
+ const headerClasses = classNames('embPanel__header', {
+ 'embPanel__header--floater': !showPanelBar,
+ });
+
+ const titleClasses = classNames('embPanel__title', {
+ 'embPanel--dragHandle': viewMode === ViewMode.EDIT,
+ });
+
+ const embeddablePanelContextMenu = (
+
+ );
+
+ if (!showPanelBar) {
+ return (
+
+ {embeddablePanelContextMenu}
+ {ariaLabelElement}
+
+ );
+ }
+
+ return (
+
+
+ {ariaLabelElement}
+
+ {showBadges && badgeComponents}
+
+ {showNotifications && notificationComponents}
+ {embeddablePanelContextMenu}
+
+ );
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_title.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_title.tsx
new file mode 100644
index 000000000000..968aa11de9c3
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_title.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import classNames from 'classnames';
+import React, { useMemo } from 'react';
+import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
+
+import { IEmbeddable, ViewMode } from '../../lib';
+import { CustomizePanelAction } from '../panel_actions';
+import { getEditTitleAriaLabel, placeholderTitle } from '../embeddable_panel_strings';
+
+export const EmbeddablePanelTitle = ({
+ viewMode,
+ hideTitle,
+ embeddable,
+ description,
+ customizePanelAction,
+}: {
+ hideTitle?: boolean;
+ viewMode?: ViewMode;
+ description?: string;
+ embeddable: IEmbeddable;
+ customizePanelAction?: CustomizePanelAction;
+}) => {
+ const title = embeddable.getTitle();
+
+ const titleComponent = useMemo(() => {
+ if (hideTitle) return null;
+ const titleClassNames = classNames('embPanel__titleText', {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ embPanel__placeholderTitleText: !title,
+ });
+
+ if (viewMode === ViewMode.VIEW) {
+ return {title};
+ }
+ if (customizePanelAction) {
+ return (
+ customizePanelAction.execute({ embeddable })}
+ >
+ {title || placeholderTitle}
+
+ );
+ }
+ return null;
+ }, [customizePanelAction, embeddable, title, viewMode, hideTitle]);
+
+ const titleComponentWithDescription = useMemo(() => {
+ if (!description) return {titleComponent};
+ return (
+
+
+ {titleComponent}
+
+
+ );
+ }, [description, titleComponent]);
+
+ return titleComponentWithDescription;
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_header/use_embeddable_panel_badges.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_header/use_embeddable_panel_badges.tsx
new file mode 100644
index 000000000000..fd2d204e9af6
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_header/use_embeddable_panel_badges.tsx
@@ -0,0 +1,165 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Subscription } from 'rxjs';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { EuiBadge, EuiNotificationBadge, EuiToolTip } from '@elastic/eui';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+
+import {
+ IEmbeddable,
+ panelBadgeTrigger,
+ panelNotificationTrigger,
+ PANEL_BADGE_TRIGGER,
+ PANEL_NOTIFICATION_TRIGGER,
+} from '../..';
+import { uiActions } from '../../kibana_services';
+import {
+ EmbeddableBadgeAction,
+ EmbeddableNotificationAction,
+ EmbeddablePanelProps,
+} from '../types';
+
+export const useEmbeddablePanelBadges = (
+ embeddable: IEmbeddable,
+ getActions: EmbeddablePanelProps['getActions']
+) => {
+ const getActionsForTrigger = getActions ?? uiActions.getTriggerCompatibleActions;
+
+ const [badges, setBadges] = useState();
+ const [notifications, setNotifications] = useState();
+
+ const getAllBadgesFromEmbeddable = useCallback(async () => {
+ let currentBadges: EmbeddableBadgeAction[] =
+ ((await getActionsForTrigger(PANEL_BADGE_TRIGGER, {
+ embeddable,
+ })) as EmbeddableBadgeAction[]) ?? [];
+
+ const { disabledActions } = embeddable.getInput();
+ if (disabledActions) {
+ currentBadges = currentBadges.filter((badge) => disabledActions.indexOf(badge.id) === -1);
+ }
+ return currentBadges;
+ }, [embeddable, getActionsForTrigger]);
+
+ const getAllNotificationsFromEmbeddable = useCallback(async () => {
+ let currentNotifications: EmbeddableNotificationAction[] =
+ ((await getActionsForTrigger(PANEL_NOTIFICATION_TRIGGER, {
+ embeddable,
+ })) as EmbeddableNotificationAction[]) ?? [];
+
+ const { disabledActions } = embeddable.getInput();
+ if (disabledActions) {
+ currentNotifications = currentNotifications.filter(
+ (badge) => disabledActions.indexOf(badge.id) === -1
+ );
+ }
+ return currentNotifications;
+ }, [embeddable, getActionsForTrigger]);
+
+ /**
+ * On embeddable creation get initial badges & notifications then subscribe to all
+ * input updates to refresh them
+ */
+ useEffect(() => {
+ let canceled = false;
+ let subscription: Subscription;
+
+ const updateNotificationsAndBadges = async () => {
+ const [newBadges, newNotifications] = await Promise.all([
+ getAllBadgesFromEmbeddable(),
+ getAllNotificationsFromEmbeddable(),
+ ]);
+ if (canceled) return;
+ setBadges(newBadges);
+ setNotifications(newNotifications);
+ };
+
+ updateNotificationsAndBadges().then(() => {
+ if (canceled) return;
+
+ /**
+ * since any piece of state could theoretically change which actions are available we need to
+ * recalculate them on any input change or any parent input change.
+ */
+ subscription = embeddable.getInput$().subscribe(() => updateNotificationsAndBadges());
+ if (embeddable.parent) {
+ subscription.add(
+ embeddable.parent.getInput$().subscribe(() => updateNotificationsAndBadges())
+ );
+ }
+ });
+ return () => {
+ subscription?.unsubscribe();
+ canceled = true;
+ };
+ }, [embeddable, getAllBadgesFromEmbeddable, getAllNotificationsFromEmbeddable]);
+
+ const badgeComponents = useMemo(
+ () =>
+ badges?.map((badge) => (
+ badge.execute({ embeddable, trigger: panelBadgeTrigger })}
+ onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
+ data-test-subj={`embeddablePanelBadge-${badge.id}`}
+ >
+ {badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
+
+ )),
+ [badges, embeddable]
+ );
+
+ const notificationComponents = useMemo(
+ () =>
+ notifications?.map((notification) => {
+ const context = { embeddable };
+
+ let badge = notification.MenuItem ? (
+ React.createElement(notification.MenuItem, {
+ key: notification.id,
+ context: {
+ embeddable,
+ trigger: panelNotificationTrigger,
+ },
+ })
+ ) : (
+ notification.execute({ ...context, trigger: panelNotificationTrigger })}
+ >
+ {notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })}
+
+ );
+
+ if (notification.getDisplayNameTooltip) {
+ const tooltip = notification.getDisplayNameTooltip({
+ ...context,
+ trigger: panelNotificationTrigger,
+ });
+
+ if (tooltip) {
+ badge = (
+
+ {badge}
+
+ );
+ }
+ }
+
+ return badge;
+ }),
+ [embeddable, notifications]
+ );
+
+ return { badgeComponents, notificationComponents };
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/types.ts b/src/plugins/embeddable/public/embeddable_panel/types.ts
new file mode 100644
index 000000000000..9a1b17d4a3a4
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/types.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ReactNode } from 'react';
+import { MaybePromise } from '@kbn/utility-types';
+import { Action, UiActionsService } from '@kbn/ui-actions-plugin/public';
+
+import {
+ EditPanelAction,
+ RemovePanelAction,
+ InspectPanelAction,
+ CustomizePanelAction,
+} from './panel_actions';
+import { EmbeddableError } from '../lib/embeddables/i_embeddable';
+import { EmbeddableContext, EmbeddableInput, EmbeddableOutput, IEmbeddable } from '..';
+
+export interface EmbeddableContainerContext {
+ /**
+ * Current app's path including query and hash starting from {appId}
+ */
+ getCurrentPath?: () => string;
+}
+
+/**
+ * Performance tracking types
+ */
+export type EmbeddablePhase = 'loading' | 'loaded' | 'rendered' | 'error';
+export interface EmbeddablePhaseEvent {
+ id: string;
+ status: EmbeddablePhase;
+ error?: EmbeddableError;
+ timeToEvent: number;
+}
+
+export type EmbeddableBadgeAction = Action<
+ EmbeddableContext>
+>;
+
+export type EmbeddableNotificationAction = Action<
+ EmbeddableContext>
+>;
+
+type PanelEmbeddable = IEmbeddable>;
+
+export interface EmbeddablePanelProps {
+ showBadges?: boolean;
+ showShadow?: boolean;
+ hideHeader?: boolean;
+ hideInspector?: boolean;
+ showNotifications?: boolean;
+ containerContext?: EmbeddableContainerContext;
+ actionPredicate?: (actionId: string) => boolean;
+ onPanelStatusChange?: (info: EmbeddablePhaseEvent) => void;
+ getActions?: UiActionsService['getTriggerCompatibleActions'];
+ embeddable: PanelEmbeddable | (() => Promise);
+
+ /**
+ * Ordinal number of the embeddable in the container, used as a
+ * "title" when the panel has no title, i.e. "Panel {index}".
+ */
+ index?: number;
+}
+
+export type UnwrappedEmbeddablePanelProps = Omit & {
+ embeddable: PanelEmbeddable;
+};
+
+export interface InspectorPanelAction {
+ inspectPanel: InspectPanelAction;
+}
+
+export interface BasePanelActions {
+ customizePanel: CustomizePanelAction;
+ inspectPanel: InspectPanelAction;
+ removePanel: RemovePanelAction;
+ editPanel: EditPanelAction;
+}
+
+export interface PanelUniversalActions
+ extends Partial,
+ Partial {}
diff --git a/src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.test.ts b/src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.test.ts
new file mode 100644
index 000000000000..3ee7bd3e3d09
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.test.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { renderHook } from '@testing-library/react-hooks';
+
+import * as kibanaServices from '../kibana_services';
+import { ErrorEmbeddable, IEmbeddable } from '../lib';
+import { useEmbeddablePanel } from './use_embeddable_panel';
+
+jest.mock('../kibana_services');
+
+describe('useEmbeddablePanel', () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('returns the correct values when an unwrapped embeddable is provided', async () => {
+ const embeddable: IEmbeddable = { id: 'supertest' } as unknown as IEmbeddable;
+
+ const { result, waitForNextUpdate } = renderHook(() => useEmbeddablePanel({ embeddable }));
+
+ await waitForNextUpdate();
+
+ expect(result.current).toBeDefined();
+ expect(result.current!.unwrappedEmbeddable).toEqual(embeddable);
+ expect(result.current!.Panel).toBeDefined();
+ });
+
+ it('returns the correct values when embeddable is provided as an async function', async () => {
+ const unwrappedEmbeddable: IEmbeddable = { id: 'supertest' } as unknown as IEmbeddable;
+ const embeddable = jest
+ .fn, []>()
+ .mockResolvedValue(unwrappedEmbeddable);
+
+ const { result, waitForNextUpdate } = renderHook(() => useEmbeddablePanel({ embeddable }));
+
+ await waitForNextUpdate();
+
+ expect(result.current).toBeDefined();
+ expect(embeddable).toHaveBeenCalled();
+ expect(result.current!.Panel).toBeDefined();
+ expect(result.current!.unwrappedEmbeddable).toEqual(unwrappedEmbeddable);
+ });
+
+ it('calls untilPluginStartServicesReady', async () => {
+ const embeddable: IEmbeddable = { id: 'supertest' } as unknown as IEmbeddable;
+ const untilPluginStartServicesReadySpy = jest.spyOn(
+ kibanaServices,
+ 'untilPluginStartServicesReady'
+ );
+
+ const { waitForNextUpdate } = renderHook(() => useEmbeddablePanel({ embeddable }));
+ await waitForNextUpdate();
+
+ expect(untilPluginStartServicesReadySpy).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.ts b/src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.ts
new file mode 100644
index 000000000000..3386cf41bab7
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import useAsync from 'react-use/lib/useAsync';
+
+import { IEmbeddable } from '../lib';
+import { EmbeddablePanelProps, UnwrappedEmbeddablePanelProps } from './types';
+import { untilPluginStartServicesReady } from '../kibana_services';
+
+export type UseEmbeddablePanelResult =
+ | {
+ unwrappedEmbeddable: IEmbeddable;
+ Panel: (props: UnwrappedEmbeddablePanelProps) => JSX.Element;
+ }
+ | undefined;
+
+export const useEmbeddablePanel = ({
+ embeddable,
+}: {
+ embeddable: EmbeddablePanelProps['embeddable'];
+}): UseEmbeddablePanelResult => {
+ const { loading, value } = useAsync(async () => {
+ const startServicesPromise = untilPluginStartServicesReady();
+ const modulePromise = import('./embeddable_panel');
+ const embeddablePromise =
+ typeof embeddable === 'function' ? embeddable() : Promise.resolve(embeddable);
+ const [, unwrappedEmbeddable, panelModule] = await Promise.all([
+ startServicesPromise,
+ embeddablePromise,
+ modulePromise,
+ ]);
+ return { panelModule, unwrappedEmbeddable };
+ }, []);
+
+ if (loading || !value?.panelModule || !value?.unwrappedEmbeddable) return;
+ return {
+ unwrappedEmbeddable: value.unwrappedEmbeddable,
+ Panel: value.panelModule.EmbeddablePanel,
+ };
+};
diff --git a/src/plugins/embeddable/public/embeddable_panel/use_select_from_embeddable.ts b/src/plugins/embeddable/public/embeddable_panel/use_select_from_embeddable.ts
new file mode 100644
index 000000000000..e280b21bbb43
--- /dev/null
+++ b/src/plugins/embeddable/public/embeddable_panel/use_select_from_embeddable.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useEffect, useState } from 'react';
+import { distinctUntilKeyChanged } from 'rxjs';
+import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../lib';
+
+export const useSelectFromEmbeddableInput = <
+ InputType extends EmbeddableInput,
+ KeyType extends keyof InputType
+>(
+ key: KeyType,
+ embeddable?: IEmbeddable
+): InputType[KeyType] | undefined => {
+ const [value, setValue] = useState(embeddable?.getInput()[key]);
+ useEffect(() => {
+ const subscription = embeddable
+ ?.getInput$()
+ .pipe(distinctUntilKeyChanged(key))
+ .subscribe(() => setValue(embeddable.getInput()[key]));
+ return () => subscription?.unsubscribe();
+ }, [embeddable, key]);
+
+ return value;
+};
+
+export const useSelectFromEmbeddableOutput = <
+ OutputType extends EmbeddableOutput,
+ KeyType extends keyof OutputType
+>(
+ key: KeyType,
+ embeddable: IEmbeddable
+): OutputType[KeyType] => {
+ const [value, setValue] = useState(embeddable.getOutput()[key]);
+ useEffect(() => {
+ const subscription = embeddable
+ .getOutput$()
+ .pipe(distinctUntilKeyChanged(key))
+ .subscribe(() => setValue(embeddable.getOutput()[key]));
+ return () => subscription.unsubscribe();
+ }, [embeddable, key]);
+
+ return value;
+};
diff --git a/src/plugins/embeddable/public/index.scss b/src/plugins/embeddable/public/index.scss
index ed80b3f9983e..6c0ad7167a08 100644
--- a/src/plugins/embeddable/public/index.scss
+++ b/src/plugins/embeddable/public/index.scss
@@ -1,3 +1,2 @@
@import './variables';
-@import './lib/panel/index';
-@import './lib/panel/panel_header/index';
+@import './embeddable_panel/embeddable_panel';
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index 9d89aab70476..7a9ecda475f6 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -19,10 +19,7 @@ export type {
ChartActionContext,
ContainerInput,
ContainerOutput,
- EmbeddableChildPanelProps,
EmbeddableContext,
- EmbeddablePhaseEvent,
- EmbeddablePhase,
EmbeddableFactory,
EmbeddableFactoryDefinition,
EmbeddableInput,
@@ -41,28 +38,20 @@ export type {
EmbeddableEditorState,
EmbeddablePackageState,
EmbeddableRendererProps,
- EmbeddableContainerContext,
EmbeddableContainerSettings,
} from './lib';
export {
- ACTION_ADD_PANEL,
- ACTION_EDIT_PANEL,
- AddPanelAction,
isReferenceOrValueEmbeddable,
Container,
CONTEXT_MENU_TRIGGER,
contextMenuTrigger,
defaultEmbeddableFactoryProvider,
- EditPanelAction,
Embeddable,
- EmbeddableChildPanel,
EmbeddableFactoryNotFoundError,
- EmbeddablePanel,
EmbeddableRoot,
ErrorEmbeddable,
isEmbeddable,
isErrorEmbeddable,
- openAddPanelFlyout,
PANEL_BADGE_TRIGGER,
panelBadgeTrigger,
PANEL_NOTIFICATION_TRIGGER,
@@ -93,6 +82,24 @@ export {
panelHoverTrigger,
} from './lib';
+export { EmbeddablePanel } from './embeddable_panel';
+export {
+ InspectPanelAction,
+ ACTION_INSPECT_PANEL,
+ CustomizePanelAction,
+ ACTION_CUSTOMIZE_PANEL,
+ EditPanelAction,
+ ACTION_EDIT_PANEL,
+ RemovePanelAction,
+ REMOVE_PANEL_ACTION,
+} from './embeddable_panel/panel_actions';
+
+export type {
+ EmbeddablePhase,
+ EmbeddablePhaseEvent,
+ EmbeddableContainerContext,
+} from './embeddable_panel/types';
+
export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
export type { EnhancementRegistryDefinition } from './types';
@@ -101,10 +108,11 @@ export function plugin(initializerContext: PluginInitializerContext) {
return new EmbeddablePublicPlugin(initializerContext);
}
+export { openAddPanelFlyout } from './add_panel_flyout/open_add_panel_flyout';
+
export type {
EmbeddableSetup,
EmbeddableStart,
EmbeddableSetupDependencies,
EmbeddableStartDependencies,
- EmbeddablePanelHOC,
} from './plugin';
diff --git a/src/plugins/embeddable/public/kibana_services.ts b/src/plugins/embeddable/public/kibana_services.ts
new file mode 100644
index 000000000000..32428bb9c237
--- /dev/null
+++ b/src/plugins/embeddable/public/kibana_services.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { BehaviorSubject } from 'rxjs';
+
+import { CoreStart } from '@kbn/core/public';
+
+import { EmbeddableStart, EmbeddableStartDependencies } from '.';
+
+export let core: CoreStart;
+export let embeddableStart: EmbeddableStart;
+export let uiActions: EmbeddableStartDependencies['uiActions'];
+export let inspector: EmbeddableStartDependencies['inspector'];
+export let usageCollection: EmbeddableStartDependencies['usageCollection'];
+export let savedObjectsManagement: EmbeddableStartDependencies['savedObjectsManagement'];
+export let savedObjectsTaggingOss: EmbeddableStartDependencies['savedObjectsTaggingOss'];
+
+const servicesReady$ = new BehaviorSubject(false);
+export const untilPluginStartServicesReady = () => {
+ if (servicesReady$.value) return Promise.resolve();
+ return new Promise((resolve) => {
+ const subscription = servicesReady$.subscribe((isInitialized) => {
+ if (isInitialized) {
+ subscription.unsubscribe();
+ resolve();
+ }
+ });
+ });
+};
+
+export const setKibanaServices = (
+ kibanaCore: CoreStart,
+ selfStart: EmbeddableStart,
+ deps: EmbeddableStartDependencies
+) => {
+ core = kibanaCore;
+ uiActions = deps.uiActions;
+ inspector = deps.inspector;
+ embeddableStart = selfStart;
+ usageCollection = deps.usageCollection;
+ savedObjectsManagement = deps.savedObjectsManagement;
+ savedObjectsTaggingOss = deps.savedObjectsTaggingOss;
+
+ servicesReady$.next(true);
+};
diff --git a/src/plugins/embeddable/public/lib/actions/index.ts b/src/plugins/embeddable/public/lib/actions/index.ts
deleted file mode 100644
index d6e1c06129ef..000000000000
--- a/src/plugins/embeddable/public/lib/actions/index.ts
+++ /dev/null
@@ -1,9 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-export * from './edit_panel_action';
diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
deleted file mode 100644
index fb2a35e634da..000000000000
--- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx
+++ /dev/null
@@ -1,97 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import React from 'react';
-import { EmbeddableChildPanel } from './embeddable_child_panel';
-import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
-import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory';
-import { HelloWorldContainer } from '../test_samples/embeddables/hello_world_container';
-import {
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable,
-} from '../test_samples/embeddables/contact_card/contact_card_embeddable';
-import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
-import { mount } from 'enzyme';
-import { embeddablePluginMock, createEmbeddablePanelMock } from '../../mocks';
-
-test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => {
- const inspector = inspectorPluginMock.createStartContract();
- const { setup, doStart } = embeddablePluginMock.createInstance();
- setup.registerEmbeddableFactory(
- CONTACT_CARD_EMBEDDABLE,
- new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any })
- );
- const start = doStart();
- const getEmbeddableFactory = start.getEmbeddableFactory;
-
- const container = new HelloWorldContainer({ id: 'hello', panels: {} }, {
- getEmbeddableFactory,
- } as any);
- const newEmbeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Theon',
- lastName: 'Greyjoy',
- id: '123',
- });
-
- expect(newEmbeddable.id).toBeDefined();
-
- const testPanel = createEmbeddablePanelMock({
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- getEmbeddableFactory,
- inspector,
- });
-
- const component = mount(
-
- );
-
- await new Promise((r) => setTimeout(r, 1));
- component.update();
-
- // Due to the way embeddables mount themselves on the dom node, they are not forced to be
- // react components, and hence, we can't use the usual
- // findTestSubject(component, 'embeddablePanelHeading-HelloTheonGreyjoy');
- expect(
- component
- .getDOMNode()
- .querySelectorAll('[data-test-subj="embeddablePanelHeading-HelloTheonGreyjoy"]').length
- ).toBe(1);
-});
-
-test(`EmbeddableChildPanel renders an error message if the factory doesn't exist`, async () => {
- const inspector = inspectorPluginMock.createStartContract();
- const getEmbeddableFactory = () => undefined;
- const container = new HelloWorldContainer(
- {
- id: 'hello',
- panels: { '1': { type: 'idontexist', explicitInput: { id: '1' } } },
- },
- { getEmbeddableFactory } as any
- );
-
- const testPanel = createEmbeddablePanelMock({ inspector });
- const component = mount(
-
- );
-
- await new Promise((r) => setTimeout(r, 1));
- component.update();
-
- expect(
- component.getDOMNode().querySelectorAll('[data-test-subj="embeddableStackError"]').length
- ).toBe(1);
-});
diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx
deleted file mode 100644
index 32e39c3c2784..000000000000
--- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx
+++ /dev/null
@@ -1,140 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import classNames from 'classnames';
-import React from 'react';
-
-import { EuiLoadingChart } from '@elastic/eui';
-import { Subscription } from 'rxjs';
-import { distinct, map } from 'rxjs/operators';
-import { isNil } from 'lodash';
-import { ErrorEmbeddable, IEmbeddable } from '../embeddables';
-import { IContainer } from './i_container';
-import { EmbeddableStart } from '../../plugin';
-import { EmbeddableError, EmbeddableOutput } from '../embeddables/i_embeddable';
-
-export type EmbeddablePhase = 'loading' | 'loaded' | 'rendered' | 'error';
-export interface EmbeddablePhaseEvent {
- id: string;
- status: EmbeddablePhase;
- error?: EmbeddableError;
- timeToEvent: number;
-}
-
-export interface EmbeddableChildPanelProps {
- embeddableId: string;
- index?: number;
- className?: string;
- container: IContainer;
- PanelComponent: EmbeddableStart['EmbeddablePanel'];
- onPanelStatusChange?: (info: EmbeddablePhaseEvent) => void;
-}
-interface State {
- firstTimeLoading: boolean;
-}
-
-/**
- * This component can be used by embeddable containers using react to easily render children. It waits
- * for the child to be initialized, showing a loading indicator until that is complete.
- */
-
-export class EmbeddableChildPanel extends React.Component {
- [panel: string]: any;
- public mounted: boolean;
- public embeddable!: IEmbeddable | ErrorEmbeddable;
- private subscription: Subscription = new Subscription();
-
- constructor(props: EmbeddableChildPanelProps) {
- super(props);
- this.state = {
- firstTimeLoading: true,
- };
-
- this.mounted = false;
- }
-
- private getEventStatus(output: EmbeddableOutput): EmbeddablePhase {
- if (!isNil(output.error)) {
- return 'error';
- } else if (output.rendered === true) {
- return 'rendered';
- } else if (output.loading === false) {
- return 'loaded';
- } else {
- return 'loading';
- }
- }
-
- public async componentDidMount() {
- this.mounted = true;
- const { container } = this.props;
-
- this.embeddable = await container.untilEmbeddableLoaded(this.props.embeddableId);
-
- if (this.mounted) {
- let loadingStartTime = 0;
- this.subscription?.add(
- this.embeddable
- .getOutput$()
- .pipe(
- // Map loaded event properties
- map((output) => {
- if (output.loading === true) {
- loadingStartTime = performance.now();
- }
- return {
- id: this.embeddable.id,
- status: this.getEventStatus(output),
- error: output.error,
- };
- }),
- // Dedupe
- distinct((output) => loadingStartTime + output.id + output.status + !!output.error),
- // Map loaded event properties
- map((output): EmbeddablePhaseEvent => {
- return {
- ...output,
- timeToEvent: performance.now() - loadingStartTime,
- };
- })
- )
- .subscribe((statusOutput) => {
- if (this.props.onPanelStatusChange) {
- this.props.onPanelStatusChange(statusOutput);
- }
- })
- );
-
- this.setState({ firstTimeLoading: false });
- }
- }
-
- public componentWillUnmount() {
- this.mounted = false;
- if (this.subscription) {
- this.subscription.unsubscribe();
- }
- }
-
- public render() {
- const { PanelComponent, index } = this.props;
- const classes = classNames('embPanel', {
- 'embPanel-isLoading': this.state.firstTimeLoading,
- });
-
- return (
-
- {this.state.firstTimeLoading || !this.embeddable ? (
-
- ) : (
-
- )}
-
- );
- }
-}
diff --git a/src/plugins/embeddable/public/lib/containers/index.ts b/src/plugins/embeddable/public/lib/containers/index.ts
index 655fd413e3bc..79bb3d6f39a4 100644
--- a/src/plugins/embeddable/public/lib/containers/index.ts
+++ b/src/plugins/embeddable/public/lib/containers/index.ts
@@ -14,4 +14,3 @@ export type {
EmbeddableContainerSettings,
} from './i_container';
export { Container } from './container';
-export * from './embeddable_child_panel';
diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
index 244803c5e13f..d18ef811586b 100644
--- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
@@ -7,10 +7,12 @@
*/
import React, { ReactNode } from 'react';
-import { EmbeddablePanelError } from '../panel/embeddable_panel_error';
+
import { Embeddable } from './embeddable';
-import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { IContainer } from '../containers';
+import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
+import { EmbeddablePanelError } from '../../embeddable_panel/embeddable_panel_error';
+
import './error_embeddable.scss';
export const ERROR_EMBEDDABLE_TYPE = 'error';
diff --git a/src/plugins/embeddable/public/lib/index.ts b/src/plugins/embeddable/public/lib/index.ts
index f8c30b12c5c7..ad25c74ca3ba 100644
--- a/src/plugins/embeddable/public/lib/index.ts
+++ b/src/plugins/embeddable/public/lib/index.ts
@@ -9,10 +9,8 @@
export * from './errors';
export * from './embeddables';
export * from './types';
-export * from './actions';
export * from './triggers';
export * from './containers';
-export * from './panel';
export * from './state_transfer';
export * from './reference_or_value_embeddable';
export * from './self_styled_embeddable';
diff --git a/src/plugins/embeddable/public/lib/panel/_index.scss b/src/plugins/embeddable/public/lib/panel/_index.scss
deleted file mode 100644
index ff8960ac729c..000000000000
--- a/src/plugins/embeddable/public/lib/panel/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import './embeddable_panel';
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
deleted file mode 100644
index 8752bcedfe00..000000000000
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
+++ /dev/null
@@ -1,787 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import React from 'react';
-import { ReactWrapper, mount } from 'enzyme';
-import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
-
-import { findTestSubject } from '@elastic/eui/lib/test';
-import { I18nProvider } from '@kbn/i18n-react';
-import { CONTEXT_MENU_TRIGGER } from '../triggers';
-import { Action, UiActionsStart, ActionInternal } from '@kbn/ui-actions-plugin/public';
-import { Trigger, ViewMode } from '../types';
-import { isErrorEmbeddable } from '../embeddables';
-import { EmbeddablePanel } from './embeddable_panel';
-import {
- createEditModeActionDefinition,
- ContactCardEmbeddable,
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddableFactory,
- ContactCardEmbeddableReactFactory,
- CONTACT_CARD_EMBEDDABLE,
- CONTACT_CARD_EMBEDDABLE_REACT,
- HelloWorldContainer,
-} from '../test_samples';
-import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
-import { EuiBadge } from '@elastic/eui';
-import { embeddablePluginMock } from '../../mocks';
-import { applicationServiceMock, themeServiceMock } from '@kbn/core/public/mocks';
-
-const actionRegistry = new Map();
-const triggerRegistry = new Map();
-
-const { setup, doStart } = embeddablePluginMock.createInstance();
-
-const editModeAction = createEditModeActionDefinition();
-const trigger: Trigger = {
- id: CONTEXT_MENU_TRIGGER,
-};
-const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
-const embeddableReactFactory = new ContactCardEmbeddableReactFactory(
- (() => null) as any,
- {} as any
-);
-const applicationMock = applicationServiceMock.createStartContract();
-const theme = themeServiceMock.createStartContract();
-
-actionRegistry.set(editModeAction.id, new ActionInternal(editModeAction));
-triggerRegistry.set(trigger.id, trigger);
-setup.registerEmbeddableFactory(embeddableFactory.type, embeddableFactory);
-setup.registerEmbeddableFactory(embeddableReactFactory.type, embeddableReactFactory);
-
-const start = doStart();
-const getEmbeddableFactory = start.getEmbeddableFactory;
-test('HelloWorldContainer initializes embeddables', (done) => {
- const container = new HelloWorldContainer(
- {
- id: '123',
- panels: {
- '123': {
- explicitInput: { id: '123', firstName: 'Sam' },
- type: CONTACT_CARD_EMBEDDABLE,
- },
- },
- },
- { getEmbeddableFactory } as any
- );
-
- const subscription = container.getOutput$().subscribe(() => {
- if (container.getOutput().embeddableLoaded['123']) {
- const embeddable = container.getChild('123');
- expect(embeddable).toBeDefined();
- expect(embeddable.id).toBe('123');
- done();
- }
- });
-
- if (container.getOutput().embeddableLoaded['123']) {
- const embeddable = container.getChild('123');
- expect(embeddable).toBeDefined();
- expect(embeddable.id).toBe('123');
- subscription.unsubscribe();
- done();
- }
-});
-
-test('HelloWorldContainer.addNewEmbeddable', async () => {
- const container = new HelloWorldContainer({ id: '123', panels: {} }, {
- getEmbeddableFactory,
- } as any);
- const embeddable = await container.addNewEmbeddable(
- CONTACT_CARD_EMBEDDABLE,
- {
- firstName: 'Kibana',
- }
- );
- expect(embeddable).toBeDefined();
-
- if (!isErrorEmbeddable(embeddable)) {
- expect(embeddable.getInput().firstName).toBe('Kibana');
- } else {
- expect(false).toBe(true);
- }
-
- const embeddableInContainer = container.getChild(embeddable.id);
- expect(embeddableInContainer).toBeDefined();
- expect(embeddableInContainer.id).toBe(embeddable.id);
-});
-
-test('Container view mode change propagates to children', async () => {
- const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
- getEmbeddableFactory,
- } as any);
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Bob',
- });
-
- expect(embeddable.getInput().viewMode).toBe(ViewMode.VIEW);
-
- container.updateInput({ viewMode: ViewMode.EDIT });
-
- expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT);
-});
-
-test('HelloWorldContainer in view mode hides edit mode actions', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
- getEmbeddableFactory,
- } as any);
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Bob',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- application={applicationMock}
- overlays={{} as any}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
- );
-
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
- expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
- await nextTick();
- component.update();
- expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0);
-});
-
-describe('HelloWorldContainer in error state', () => {
- let component: ReactWrapper;
- let embeddable: ContactCardEmbeddable;
-
- beforeEach(async () => {
- const inspector = inspectorPluginMock.createStartContract();
- const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
- getEmbeddableFactory,
- } as any);
-
- embeddable = (await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {})) as ContactCardEmbeddable;
-
- component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- application={applicationMock}
- overlays={{} as any}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
- );
-
- jest.spyOn(embeddable, 'catchError');
- });
-
- test('renders a custom error', () => {
- embeddable.triggerError(new Error('something'));
- component.update();
-
- const embeddableError = findTestSubject(component, 'embeddableError');
-
- expect(embeddable.catchError).toHaveBeenCalledWith(
- new Error('something'),
- expect.any(HTMLElement)
- );
- expect(embeddableError).toHaveProperty('length', 1);
- expect(embeddableError.text()).toBe('something');
- });
-
- test('renders a custom fatal error', () => {
- embeddable.triggerError(new Error('something'), true);
- component.update();
- component.mount();
-
- const embeddableError = findTestSubject(component, 'embeddableError');
-
- expect(embeddable.catchError).toHaveBeenCalledWith(
- new Error('something'),
- expect.any(HTMLElement)
- );
- expect(embeddableError).toHaveProperty('length', 1);
- expect(embeddableError.text()).toBe('something');
- });
-
- test('destroys previous error', () => {
- const { catchError } = embeddable as Required;
- let destroyError: jest.MockedFunction>;
-
- (embeddable.catchError as jest.MockedFunction).mockImplementationOnce(
- (...args) => {
- destroyError = jest.fn(catchError(...args));
-
- return destroyError;
- }
- );
- embeddable.triggerError(new Error('something'));
- component.update();
- embeddable.triggerError(new Error('another error'));
- component.update();
-
- const embeddableError = findTestSubject(component, 'embeddableError');
-
- expect(embeddableError).toHaveProperty('length', 1);
- expect(embeddableError.text()).toBe('another error');
- expect(destroyError!).toHaveBeenCalledTimes(1);
- });
-
- test('renders a default error', async () => {
- embeddable.catchError = undefined;
- embeddable.triggerError(new Error('something'));
- component.update();
-
- const embeddableError = findTestSubject(component, 'embeddableError');
-
- expect(embeddableError).toHaveProperty('length', 1);
- expect(embeddableError.children.length).toBeGreaterThan(0);
- });
-
- test('renders a React node', () => {
- (embeddable.catchError as jest.Mock).mockReturnValueOnce(Something
);
- embeddable.triggerError(new Error('something'));
- component.update();
-
- const embeddableError = findTestSubject(component, 'embeddableError');
-
- expect(embeddableError).toHaveProperty('length', 1);
- expect(embeddableError.text()).toBe('Something');
- });
-});
-
-const renderInEditModeAndOpenContextMenu = async (
- embeddableInputs: any,
- getActions: UiActionsStart['getTriggerCompatibleActions'] = () => Promise.resolve([])
-) => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
- getEmbeddableFactory,
- } as any);
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, embeddableInputs);
-
- const component = mount(
-
- null}
- theme={theme}
- />
-
- );
-
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
- await nextTick();
- component.update();
-
- return { component };
-};
-
-test('HelloWorldContainer in edit mode hides disabledActions', async () => {
- const action = {
- id: 'FOO',
- type: 'FOO',
- getIconType: () => undefined,
- getDisplayName: () => 'foo',
- isCompatible: async () => true,
- execute: async () => {},
- order: 10,
- getHref: () => {
- return Promise.resolve(undefined);
- },
- };
- const getActions = () => Promise.resolve([action]);
-
- const { component: component1 } = await renderInEditModeAndOpenContextMenu(
- {
- firstName: 'Bob',
- },
- getActions
- );
- const { component: component2 } = await renderInEditModeAndOpenContextMenu(
- {
- firstName: 'Bob',
- disabledActions: ['FOO'],
- },
- getActions
- );
-
- const fooContextMenuActionItem1 = findTestSubject(component1, 'embeddablePanelAction-FOO');
- const fooContextMenuActionItem2 = findTestSubject(component2, 'embeddablePanelAction-FOO');
-
- expect(fooContextMenuActionItem1.length).toBe(1);
- expect(fooContextMenuActionItem2.length).toBe(0);
-});
-
-test('HelloWorldContainer hides disabled badges', async () => {
- const action = {
- id: 'BAR',
- type: 'BAR',
- getIconType: () => undefined,
- getDisplayName: () => 'bar',
- isCompatible: async () => true,
- execute: async () => {},
- order: 10,
- getHref: () => {
- return Promise.resolve(undefined);
- },
- };
- const getActions = () => Promise.resolve([action]);
-
- const { component: component1 } = await renderInEditModeAndOpenContextMenu(
- {
- firstName: 'Bob',
- },
- getActions
- );
- const { component: component2 } = await renderInEditModeAndOpenContextMenu(
- {
- firstName: 'Bob',
- disabledActions: ['BAR'],
- },
- getActions
- );
-
- expect(component1.find(EuiBadge).length).toBe(1);
- expect(component2.find(EuiBadge).length).toBe(0);
-});
-
-test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
- getEmbeddableFactory,
- } as any);
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Bob',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
- );
-
- const button = findTestSubject(component, 'embeddablePanelToggleMenuIcon');
-
- expect(button.length).toBe(1);
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
-
- expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
- await nextTick();
- component.update();
- expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0);
-
- container.updateInput({ viewMode: ViewMode.EDIT });
- await nextTick();
- component.update();
-
- // Need to close and re-open to refresh. It doesn't update automatically.
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
- await nextTick();
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
- await nextTick();
- expect(findTestSubject(component, 'embeddablePanelContextMenuOpen').length).toBe(1);
-
- container.updateInput({ viewMode: ViewMode.VIEW });
- await nextTick();
- component.update();
-
- // TODO: Fix this.
- // const action = findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`);
- // expect(action.length).toBe(1);
-});
-
-test('Panel title customize link does not exist in view mode', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Vayon',
- lastName: 'Poole',
- });
-
- const component = mountWithIntl(
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
- );
-
- const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
- expect(titleLink.length).toBe(0);
-});
-
-test('Runs customize panel action on title click when in edit mode', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.EDIT, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Vayon',
- lastName: 'Poole',
- });
-
- const component = mountWithIntl(
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
- );
-
- const titleExecute = jest.fn();
- component.setState((s: any) => ({
- ...s,
- universalActions: {
- ...s.universalActions,
- customizePanel: { execute: titleExecute, isCompatible: jest.fn() },
- },
- }));
-
- const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
- expect(titleLink.length).toBe(1);
- titleLink.simulate('click');
- await nextTick();
- expect(titleExecute).toHaveBeenCalledTimes(1);
-});
-
-test('Updates when hidePanelTitles is toggled', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Rob',
- lastName: 'Stark',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
- );
-
- let title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`);
- expect(title.length).toBe(1);
-
- await container.updateInput({ hidePanelTitles: true });
-
- await nextTick();
- component.update();
-
- title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`);
- expect(title.length).toBe(0);
-
- await container.updateInput({ hidePanelTitles: false });
-
- await nextTick();
- component.update();
-
- title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`);
- expect(title.length).toBe(1);
-});
-
-test('Respects options from SelfStyledEmbeddable', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const contactCardEmbeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Rob',
- lastName: 'Stark',
- });
-
- const selfStyledEmbeddable = embeddablePluginMock.mockSelfStyledEmbeddable(
- contactCardEmbeddable,
- { hideTitle: true }
- );
-
- // make sure the title is being hidden because of the self styling, not the container
- container.updateInput({ hidePanelTitles: false });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- inspector={inspector}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
- );
-
- const title = findTestSubject(component, `embeddablePanelHeading-HelloRobStark`);
- expect(title.length).toBe(0);
-});
-
-test('Check when hide header option is false', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Arya',
- lastName: 'Stark',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- inspector={inspector}
- SavedObjectFinder={() => null}
- hideHeader={false}
- theme={theme}
- />
-
- );
-
- const title = findTestSubject(component, `embeddablePanelHeading-HelloAryaStark`);
- expect(title.length).toBe(1);
-});
-
-test('Check when hide header option is true', async () => {
- const inspector = inspectorPluginMock.createStartContract();
-
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Arya',
- lastName: 'Stark',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={{} as any}
- inspector={inspector}
- SavedObjectFinder={() => null}
- hideHeader={true}
- theme={theme}
- />
-
- );
-
- const title = findTestSubject(component, `embeddablePanelHeading-HelloAryaStark`);
- expect(title.length).toBe(0);
-});
-
-test('Should work in minimal way rendering only the inspector action', async () => {
- const inspector = inspectorPluginMock.createStartContract();
- inspector.isAvailable = jest.fn(() => true);
-
- const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, {
- getEmbeddableFactory,
- } as any);
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE, {
- firstName: 'Arya',
- lastName: 'Stark',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- inspector={inspector}
- hideHeader={false}
- theme={theme}
- />
-
- );
-
- findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click');
- expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1);
- await nextTick();
- component.update();
- expect(findTestSubject(component, `embeddablePanelAction-openInspector`).length).toBe(1);
- const action = findTestSubject(component, `embeddablePanelAction-ACTION_CUSTOMIZE_PANEL`);
- expect(action.length).toBe(0);
-});
-
-test('Renders an embeddable returning a React node', async () => {
- const container = new HelloWorldContainer(
- { id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
- { getEmbeddableFactory } as any
- );
-
- const embeddable = await container.addNewEmbeddable<
- ContactCardEmbeddableInput,
- ContactCardEmbeddableOutput,
- ContactCardEmbeddable
- >(CONTACT_CARD_EMBEDDABLE_REACT, {
- firstName: 'Bran',
- lastName: 'Stark',
- });
-
- const component = mount(
-
- Promise.resolve([])}
- getAllEmbeddableFactories={start.getEmbeddableFactories}
- getEmbeddableFactory={start.getEmbeddableFactory}
- notifications={{} as any}
- overlays={{} as any}
- application={applicationMock}
- SavedObjectFinder={() => null}
- theme={theme}
- />
-
- );
-
- expect(component.find('.embPanel__titleText').text()).toBe('Hello Bran Stark');
-});
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
deleted file mode 100644
index ffd892488729..000000000000
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
+++ /dev/null
@@ -1,476 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import {
- EuiContextMenuPanelDescriptor,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPanel,
- htmlIdGenerator,
-} from '@elastic/eui';
-import classNames from 'classnames';
-import React, { ReactNode } from 'react';
-import { Subscription } from 'rxjs';
-import deepEqual from 'fast-deep-equal';
-import { CoreStart, ThemeServiceStart } from '@kbn/core/public';
-import { isPromise } from '@kbn/std';
-import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
-import { MaybePromise } from '@kbn/utility-types';
-import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions';
-
-import { Start as InspectorStartContract } from '../inspector';
-import {
- CONTEXT_MENU_TRIGGER,
- PANEL_BADGE_TRIGGER,
- PANEL_NOTIFICATION_TRIGGER,
- EmbeddableContext,
- contextMenuTrigger,
-} from '../triggers';
-import {
- EmbeddableErrorHandler,
- EmbeddableInput,
- EmbeddableOutput,
- IEmbeddable,
-} from '../embeddables';
-import { ViewMode } from '../types';
-
-import { EmbeddablePanelError } from './embeddable_panel_error';
-import { RemovePanelAction } from './panel_header/panel_actions';
-import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action';
-import { CustomizePanelAction } from './panel_header/panel_actions/customize_panel/customize_panel_action';
-import { PanelHeader } from './panel_header/panel_header';
-import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action';
-import { EditPanelAction } from '../actions';
-import { EmbeddableStart } from '../../plugin';
-import { EmbeddableStateTransfer, isSelfStyledEmbeddable, CommonlyUsedRange } from '..';
-
-const sortByOrderField = (
- { order: orderA }: { order?: number },
- { order: orderB }: { order?: number }
-) => (orderB || 0) - (orderA || 0);
-
-const removeById =
- (disabledActions: string[]) =>
- ({ id }: { id: string }) =>
- disabledActions.indexOf(id) === -1;
-
-/**
- * Embeddable container may provide information about its environment,
- * Use it for drilling down data that is static or doesn't have to be reactive,
- * otherwise prefer passing data with input$
- * */
-export interface EmbeddableContainerContext {
- /**
- * Current app's path including query and hash starting from {appId}
- */
- getCurrentPath?: () => string;
-}
-
-interface Props {
- embeddable: IEmbeddable>;
-
- /**
- * Ordinal number of the embeddable in the container, used as a
- * "title" when the panel has no title, i.e. "Panel {index}".
- */
- index?: number;
-
- getActions?: UiActionsService['getTriggerCompatibleActions'];
- getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'];
- getAllEmbeddableFactories?: EmbeddableStart['getEmbeddableFactories'];
- dateFormat?: string;
- commonlyUsedRanges?: CommonlyUsedRange[];
- overlays?: CoreStart['overlays'];
- notifications?: CoreStart['notifications'];
- application?: CoreStart['application'];
- inspector?: InspectorStartContract;
- SavedObjectFinder?: React.ComponentType;
- stateTransfer?: EmbeddableStateTransfer;
- hideHeader?: boolean;
- actionPredicate?: (actionId: string) => boolean;
- reportUiCounter?: UsageCollectionStart['reportUiCounter'];
- showShadow?: boolean;
- showBadges?: boolean;
- showNotifications?: boolean;
- containerContext?: EmbeddableContainerContext;
- theme: ThemeServiceStart;
-}
-
-interface State {
- panels: EuiContextMenuPanelDescriptor[];
- universalActions: PanelUniversalActions;
- focusedPanelIndex?: string;
- viewMode: ViewMode;
- hidePanelTitle: boolean;
- closeContextMenu: boolean;
- badges: Array>;
- notifications: Array>;
- loading?: boolean;
- error?: Error;
- destroyError?(): void;
- node?: ReactNode;
-}
-
-interface InspectorPanelAction {
- inspectPanel: InspectPanelAction;
-}
-
-interface BasePanelActions {
- customizePanel: CustomizePanelAction;
- addPanel: AddPanelAction;
- inspectPanel: InspectPanelAction;
- removePanel: RemovePanelAction;
- editPanel: EditPanelAction;
-}
-
-interface PanelUniversalActions extends Partial, Partial {}
-
-export class EmbeddablePanel extends React.Component {
- private embeddableRoot = React.createRef();
- private parentSubscription?: Subscription;
- private subscription: Subscription = new Subscription();
- private mounted: boolean = false;
- private generateId = htmlIdGenerator();
-
- constructor(props: Props) {
- super(props);
- const { embeddable } = this.props;
- const viewMode = embeddable.getInput().viewMode ?? ViewMode.EDIT;
- const hidePanelTitle =
- Boolean(embeddable.parent?.getInput()?.hidePanelTitles) ||
- Boolean(embeddable.getInput()?.hidePanelTitles);
-
- this.state = {
- universalActions: this.getUniversalActions(),
- panels: [],
- viewMode,
- hidePanelTitle,
- closeContextMenu: false,
- badges: [],
- notifications: [],
- };
- }
-
- private async refreshBadges() {
- if (this.props.showBadges === false) {
- return;
- }
-
- type BadgeAction = Action<
- EmbeddableContext>
- >;
-
- let badges: BadgeAction[] =
- ((await this.props.getActions?.(PANEL_BADGE_TRIGGER, {
- embeddable: this.props.embeddable,
- })) as BadgeAction[]) ?? [];
-
- if (!this.mounted) {
- return;
- }
-
- const { disabledActions } = this.props.embeddable.getInput();
- if (disabledActions) {
- badges = badges.filter((badge) => disabledActions.indexOf(badge.id) === -1);
- }
-
- if (!deepEqual(this.state.badges, badges)) {
- this.setState({
- badges,
- });
- }
- }
-
- private async refreshNotifications() {
- if (this.props.showNotifications === false) {
- return;
- }
-
- type NotificationAction = Action<
- EmbeddableContext>
- >;
-
- let notifications: NotificationAction[] =
- ((await this.props.getActions?.(PANEL_NOTIFICATION_TRIGGER, {
- embeddable: this.props.embeddable,
- })) as NotificationAction[]) ?? [];
-
- if (!this.mounted) {
- return;
- }
-
- const { disabledActions } = this.props.embeddable.getInput();
- if (disabledActions) {
- notifications = notifications.filter((badge) => disabledActions.indexOf(badge.id) === -1);
- }
-
- if (!deepEqual(this.state.notifications, notifications)) {
- this.setState({
- notifications,
- });
- }
- }
-
- public UNSAFE_componentWillMount() {
- this.mounted = true;
- const { embeddable } = this.props;
- const { parent } = embeddable;
-
- this.subscription.add(
- embeddable.getInput$().subscribe(async () => {
- if (this.mounted) {
- this.setState({
- viewMode: embeddable.getInput().viewMode ?? ViewMode.EDIT,
- });
-
- this.refreshBadges();
- this.refreshNotifications();
- }
- })
- );
-
- if (parent) {
- this.parentSubscription = parent.getInput$().subscribe(async () => {
- if (this.mounted && parent) {
- this.setState({
- hidePanelTitle:
- Boolean(embeddable.parent?.getInput()?.hidePanelTitles) ||
- Boolean(embeddable.getInput()?.hidePanelTitles),
- });
-
- this.refreshBadges();
- this.refreshNotifications();
- }
- });
- }
- }
-
- public componentWillUnmount() {
- this.mounted = false;
- this.subscription.unsubscribe();
- if (this.parentSubscription) {
- this.parentSubscription.unsubscribe();
- }
-
- this.state.destroyError?.();
- this.props.embeddable.destroy();
- }
-
- public onFocus = (focusedPanelIndex: string) => {
- this.setState({ focusedPanelIndex });
- };
-
- public onBlur = (blurredPanelIndex: string) => {
- if (this.state.focusedPanelIndex === blurredPanelIndex) {
- this.setState({ focusedPanelIndex: undefined });
- }
- };
-
- public render() {
- const viewOnlyMode = [ViewMode.VIEW, ViewMode.PRINT].includes(this.state.viewMode);
- const classes = classNames('embPanel', {
- 'embPanel--editing': !viewOnlyMode,
- 'embPanel--loading': this.state.loading,
- });
-
- const contentAttrs: { [key: string]: boolean } = {};
- if (this.state.loading) contentAttrs['data-loading'] = true;
- if (this.state.error) contentAttrs['data-error'] = true;
-
- const title = this.props.embeddable.getTitle();
- const description = this.props.embeddable.getDescription();
- const headerId = this.generateId();
-
- const selfStyledOptions = isSelfStyledEmbeddable(this.props.embeddable)
- ? this.props.embeddable.getSelfStyledOptions()
- : undefined;
-
- return (
-
- {!this.props.hideHeader && (
-
- )}
- {this.state.error && (
-
-
-
- {(error) => (
-
- )}
-
-
-
- )}
-
- {this.state.node}
-
-
- );
- }
-
- public componentDidMount() {
- if (!this.embeddableRoot.current) {
- return;
- }
-
- this.subscription.add(
- this.props.embeddable.getOutput$().subscribe(
- (output: EmbeddableOutput) => {
- if (this.mounted) {
- this.setState({
- error: output.error,
- loading: output.loading,
- });
- }
- },
- (error) => {
- if (this.mounted) {
- this.setState({ error });
- }
- }
- )
- );
-
- const node = this.props.embeddable.render(this.embeddableRoot.current) ?? undefined;
- if (isPromise(node)) {
- node.then((resolved) => {
- if (this.mounted) {
- this.setState({ node: resolved });
- }
- });
- } else {
- this.setState({ node });
- }
- }
-
- closeMyContextMenuPanel = () => {
- if (this.mounted) {
- this.setState({ closeContextMenu: true }, () => {
- if (this.mounted) {
- this.setState({ closeContextMenu: false });
- }
- });
- }
- };
-
- private getUniversalActions = (): PanelUniversalActions => {
- let actions = {};
- if (this.props.inspector) {
- actions = {
- inspectPanel: new InspectPanelAction(this.props.inspector),
- };
- }
- if (
- !this.props.getEmbeddableFactory ||
- !this.props.getAllEmbeddableFactories ||
- !this.props.overlays ||
- !this.props.notifications ||
- !this.props.SavedObjectFinder ||
- !this.props.application
- ) {
- return actions;
- }
-
- // Universal actions are exposed on the context menu for every embeddable, they bypass the trigger
- // registry.
- return {
- ...actions,
- customizePanel: new CustomizePanelAction(
- this.props.overlays,
- this.props.theme,
- this.props.commonlyUsedRanges,
- this.props.dateFormat
- ),
- addPanel: new AddPanelAction(
- this.props.getEmbeddableFactory,
- this.props.getAllEmbeddableFactories,
- this.props.overlays,
- this.props.notifications,
- this.props.SavedObjectFinder,
- this.props.theme,
- this.props.reportUiCounter
- ),
- removePanel: new RemovePanelAction(),
- editPanel: new EditPanelAction(
- this.props.getEmbeddableFactory,
- this.props.application,
- this.props.stateTransfer,
- this.props.containerContext?.getCurrentPath
- ),
- };
- };
-
- private getActionContextMenuPanel = async () => {
- let regularActions =
- (await this.props.getActions?.(CONTEXT_MENU_TRIGGER, {
- embeddable: this.props.embeddable,
- })) ?? [];
-
- const { disabledActions } = this.props.embeddable.getInput();
- if (disabledActions) {
- const removeDisabledActions = removeById(disabledActions);
- regularActions = regularActions.filter(removeDisabledActions);
- }
-
- let sortedActions = regularActions
- .concat(Object.values(this.state.universalActions || {}) as Array>)
- .sort(sortByOrderField);
-
- if (this.props.actionPredicate) {
- sortedActions = sortedActions.filter(({ id }) => this.props.actionPredicate!(id));
- }
-
- const panels = await buildContextMenuForActions({
- actions: sortedActions.map((action) => ({
- action,
- context: { embeddable: this.props.embeddable },
- trigger: contextMenuTrigger,
- })),
- closeMenu: this.closeMyContextMenuPanel,
- });
-
- return {
- panels,
- actions: sortedActions,
- };
- };
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/_index.scss b/src/plugins/embeddable/public/lib/panel/panel_header/_index.scss
deleted file mode 100644
index b6cea833f65c..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import './panel_options_menu_form';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/_panel_options_menu_form.scss b/src/plugins/embeddable/public/lib/panel/panel_header/_panel_options_menu_form.scss
deleted file mode 100644
index cdf0fb79f732..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/_panel_options_menu_form.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.embPanel__optionsMenuForm {
- padding: $euiSize;
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/index.ts
deleted file mode 100644
index fd06a4e4ad5d..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/index.ts
+++ /dev/null
@@ -1,9 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-export * from './panel_actions';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
deleted file mode 100644
index bcb56d5b63bc..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
+++ /dev/null
@@ -1,125 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { ViewMode, EmbeddableOutput, isErrorEmbeddable } from '../../../..';
-import { AddPanelAction } from './add_panel_action';
-import {
- MockFilter,
- FILTERABLE_EMBEDDABLE,
- FilterableEmbeddable,
- FilterableEmbeddableInput,
-} from '../../../../test_samples/embeddables/filterable_embeddable';
-import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddables/filterable_embeddable_factory';
-import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container';
-import { coreMock, themeServiceMock } from '@kbn/core/public/mocks';
-import { ContactCardEmbeddable } from '../../../../test_samples';
-import { EmbeddableStart } from '../../../../../plugin';
-import { embeddablePluginMock } from '../../../../../mocks';
-import { defaultTrigger } from '@kbn/ui-actions-browser/src/triggers/default_trigger';
-
-const { setup, doStart } = embeddablePluginMock.createInstance();
-setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
-const getFactory = doStart().getEmbeddableFactory;
-const theme = themeServiceMock.createStartContract();
-
-let container: FilterableContainer;
-let embeddable: FilterableEmbeddable;
-let action: AddPanelAction;
-
-beforeEach(async () => {
- const start = coreMock.createStart();
- action = new AddPanelAction(
- () => undefined,
- () => [] as any,
- start.overlays,
- start.notifications,
- () => null,
- theme
- );
-
- const derivedFilter: MockFilter = {
- $state: { store: 'appState' },
- meta: { disabled: false, alias: 'name', negate: false },
- query: { match: {} },
- };
- container = new FilterableContainer(
- { id: 'hello', panels: {}, filters: [derivedFilter] },
- getFactory as EmbeddableStart['getEmbeddableFactory']
- );
-
- const filterableEmbeddable = await container.addNewEmbeddable<
- FilterableEmbeddableInput,
- EmbeddableOutput,
- FilterableEmbeddable
- >(FILTERABLE_EMBEDDABLE, {
- id: '123',
- });
-
- if (isErrorEmbeddable(filterableEmbeddable)) {
- throw new Error('Error creating new filterable embeddable');
- } else {
- embeddable = filterableEmbeddable;
- }
-});
-
-test('Is not compatible when container is in view mode', async () => {
- const start = coreMock.createStart();
- const addPanelAction = new AddPanelAction(
- () => undefined,
- () => [] as any,
- start.overlays,
- start.notifications,
- () => null,
- theme
- );
- container.updateInput({ viewMode: ViewMode.VIEW });
- expect(
- await addPanelAction.isCompatible({ embeddable: container, trigger: defaultTrigger })
- ).toBe(false);
-});
-
-test('Is not compatible when embeddable is not a container', async () => {
- expect(await action.isCompatible({ embeddable } as any)).toBe(false);
-});
-
-test('Is compatible when embeddable is a parent and in edit mode', async () => {
- container.updateInput({ viewMode: ViewMode.EDIT });
- expect(await action.isCompatible({ embeddable: container, trigger: defaultTrigger })).toBe(true);
-});
-
-test('Execute throws an error when called with an embeddable that is not a container', async () => {
- async function check() {
- await action.execute({
- embeddable: new ContactCardEmbeddable(
- {
- firstName: 'sue',
- id: '123',
- viewMode: ViewMode.EDIT,
- },
- {} as any
- ),
- trigger: defaultTrigger,
- } as any);
- }
- await expect(check()).rejects.toThrow(Error);
-});
-test('Execute does not throw an error when called with a compatible container', async () => {
- container.updateInput({ viewMode: ViewMode.EDIT });
- await action.execute({
- embeddable: container,
- trigger: defaultTrigger,
- });
-});
-
-test('Returns title', async () => {
- expect(action.getDisplayName()).toBeDefined();
-});
-
-test('Returns an icon', async () => {
- expect(action.getIconType()).toBeDefined();
-});
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
deleted file mode 100644
index 2c1c31b17764..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
+++ /dev/null
@@ -1,70 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
-import { NotificationsStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
-import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
-import { EmbeddableStart } from '../../../../../plugin';
-import { ViewMode } from '../../../../types';
-import { openAddPanelFlyout } from './open_add_panel_flyout';
-import { IContainer } from '../../../../containers';
-
-export const ACTION_ADD_PANEL = 'ACTION_ADD_PANEL';
-
-interface ActionContext {
- embeddable: IContainer;
-}
-
-export class AddPanelAction implements Action {
- public readonly type = ACTION_ADD_PANEL;
- public readonly id = ACTION_ADD_PANEL;
-
- constructor(
- private readonly getFactory: EmbeddableStart['getEmbeddableFactory'],
- private readonly getAllFactories: EmbeddableStart['getEmbeddableFactories'],
- private readonly overlays: OverlayStart,
- private readonly notifications: NotificationsStart,
- private readonly SavedObjectFinder: React.ComponentType,
- private readonly theme: ThemeServiceStart,
- private readonly reportUiCounter?: UsageCollectionStart['reportUiCounter']
- ) {}
-
- public getDisplayName() {
- return i18n.translate('embeddableApi.addPanel.displayName', {
- defaultMessage: 'Add panel',
- });
- }
-
- public getIconType() {
- return 'plusInCircleFilled';
- }
-
- public async isCompatible(context: ActionExecutionContext) {
- const { embeddable } = context;
- return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT;
- }
-
- public async execute(context: ActionExecutionContext) {
- const { embeddable } = context;
- if (!embeddable.getIsContainer() || !(await this.isCompatible(context))) {
- throw new Error('Context is incompatible');
- }
-
- openAddPanelFlyout({
- embeddable,
- getFactory: this.getFactory,
- getAllFactories: this.getAllFactories,
- overlays: this.overlays,
- notifications: this.notifications,
- SavedObjectFinder: this.SavedObjectFinder,
- reportUiCounter: this.reportUiCounter,
- theme: this.theme,
- });
- }
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx
deleted file mode 100644
index 59b2af392283..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx
+++ /dev/null
@@ -1,129 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import * as React from 'react';
-import { EuiFlyout } from '@elastic/eui';
-import { AddPanelFlyout } from './add_panel_flyout';
-import {
- ContactCardEmbeddableFactory,
- CONTACT_CARD_EMBEDDABLE,
-} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
-import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container';
-import { ContactCardEmbeddable } from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable';
-import { ContainerInput } from '../../../../containers';
-import { mountWithIntl as mount } from '@kbn/test-jest-helpers';
-import { ReactWrapper } from 'enzyme';
-import { coreMock } from '@kbn/core/public/mocks';
-import { findTestSubject } from '@elastic/eui/lib/test';
-import { embeddablePluginMock } from '../../../../../mocks';
-
-function DummySavedObjectFinder(props: { children: React.ReactNode }) {
- return (
-
-
Hello World
- {props.children}
-
- ) as JSX.Element;
-}
-
-test('createNewEmbeddable() add embeddable to container', async () => {
- const { setup, doStart } = embeddablePluginMock.createInstance();
- const core = coreMock.createStart();
- const { overlays } = core;
- const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
- (() => null) as any,
- overlays
- );
- contactCardEmbeddableFactory.getExplicitInput = () =>
- ({
- firstName: 'foo',
- lastName: 'bar',
- } as any);
- setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory);
- const start = doStart();
- const getEmbeddableFactory = start.getEmbeddableFactory;
- const input: ContainerInput<{ firstName: string; lastName: string }> = {
- id: '1',
- panels: {},
- };
- const container = new HelloWorldContainer(input, { getEmbeddableFactory } as any);
- const onClose = jest.fn();
- const component = mount(
- null}
- showCreateNewMenu
- />
- ) as ReactWrapper;
-
- // https://github.com/elastic/kibana/issues/64789
- expect(component.exists(EuiFlyout)).toBe(false);
-
- expect(Object.values(container.getInput().panels).length).toBe(0);
- component.instance().createNewEmbeddable(CONTACT_CARD_EMBEDDABLE);
- await new Promise((r) => setTimeout(r, 1));
-
- const ids = Object.keys(container.getInput().panels);
- const embeddableId = ids[0];
- const child = container.getChild(embeddableId);
-
- expect(child.getInput()).toMatchObject({
- firstName: 'foo',
- lastName: 'bar',
- });
-});
-
-test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()', async () => {
- const { setup, doStart } = embeddablePluginMock.createInstance();
- const core = coreMock.createStart();
- const { overlays } = core;
- const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
- (() => null) as any,
- overlays
- );
- contactCardEmbeddableFactory.getExplicitInput = () =>
- ({
- firstName: 'foo',
- lastName: 'bar',
- } as any);
-
- setup.registerEmbeddableFactory(CONTACT_CARD_EMBEDDABLE, contactCardEmbeddableFactory);
- const start = doStart();
- const getEmbeddableFactory = start.getEmbeddableFactory;
- const input: ContainerInput<{ firstName: string; lastName: string }> = {
- id: '1',
- panels: {},
- };
- const container = new HelloWorldContainer(input, { getEmbeddableFactory } as any);
- const onClose = jest.fn();
- const component = mount(
- }
- showCreateNewMenu
- />
- ) as ReactWrapper;
-
- const spy = jest.fn();
- component.instance().createNewEmbeddable = spy;
-
- expect(spy).toHaveBeenCalledTimes(0);
-
- findTestSubject(component, 'createNew').simulate('click');
- findTestSubject(component, `createNew-${CONTACT_CARD_EMBEDDABLE}`).simulate('click');
-
- expect(spy).toHaveBeenCalledTimes(1);
-});
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
deleted file mode 100644
index ea7c150bf38b..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx
+++ /dev/null
@@ -1,191 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-import React, { ReactElement } from 'react';
-import { METRIC_TYPE } from '@kbn/analytics';
-import { CoreSetup, SavedObjectAttributes, SimpleSavedObject, Toast } from '@kbn/core/public';
-
-import { EuiContextMenuItem, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
-
-import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
-import { EmbeddableFactory, EmbeddableStart } from '../../../../..';
-import { IContainer } from '../../../../containers';
-import { EmbeddableFactoryNotFoundError } from '../../../../errors';
-import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new';
-import { SavedObjectEmbeddableInput } from '../../../../embeddables';
-
-interface Props {
- onClose: () => void;
- container: IContainer;
- getFactory: EmbeddableStart['getEmbeddableFactory'];
- getAllFactories: EmbeddableStart['getEmbeddableFactories'];
- notifications: CoreSetup['notifications'];
- SavedObjectFinder: React.ComponentType;
- showCreateNewMenu?: boolean;
- reportUiCounter?: UsageCollectionStart['reportUiCounter'];
- onAddPanel?: (id: string) => void;
-}
-
-interface State {
- isCreateMenuOpen: boolean;
-}
-
-function capitalize([first, ...letters]: string) {
- return `${first.toUpperCase()}${letters.join('')}`;
-}
-
-export class AddPanelFlyout extends React.Component {
- private lastToast?: string | Toast;
-
- public state = {
- isCreateMenuOpen: false,
- };
-
- constructor(props: Props) {
- super(props);
- }
-
- public showToast = (name: string) => {
- // To avoid the clutter of having toast messages cover flyout
- // close previous toast message before creating a new one
- if (this.lastToast) {
- this.props.notifications.toasts.remove(this.lastToast);
- }
-
- this.lastToast = this.props.notifications.toasts.addSuccess({
- title: i18n.translate(
- 'embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle',
- {
- defaultMessage: '{savedObjectName} was added',
- values: {
- savedObjectName: name,
- },
- }
- ),
- 'data-test-subj': 'addObjectToContainerSuccess',
- });
- };
-
- public createNewEmbeddable = async (type: string) => {
- this.props.onClose();
- const factory = this.props.getFactory(type);
-
- if (!factory) {
- throw new EmbeddableFactoryNotFoundError(type);
- }
-
- const explicitInput = await factory.getExplicitInput();
- const embeddable = await this.props.container.addNewEmbeddable(type, explicitInput);
- if (embeddable) {
- this.showToast(embeddable.getInput().title || '');
- }
- };
-
- public onAddPanel = async (
- savedObjectId: string,
- savedObjectType: string,
- name: string,
- so: SimpleSavedObject
- ) => {
- const factoryForSavedObjectType = [...this.props.getAllFactories()].find(
- (factory) =>
- factory.savedObjectMetaData && factory.savedObjectMetaData.type === savedObjectType
- );
- if (!factoryForSavedObjectType) {
- throw new EmbeddableFactoryNotFoundError(savedObjectType);
- }
-
- const embeddable = await this.props.container.addNewEmbeddable(
- factoryForSavedObjectType.type,
- { savedObjectId }
- );
-
- this.doTelemetryForAddEvent(this.props.container.type, factoryForSavedObjectType, so);
-
- this.showToast(name);
- if (this.props.onAddPanel) {
- this.props.onAddPanel(embeddable.id);
- }
- };
-
- private doTelemetryForAddEvent(
- appName: string,
- factoryForSavedObjectType: EmbeddableFactory,
- so: SimpleSavedObject
- ) {
- const { reportUiCounter } = this.props;
-
- if (reportUiCounter) {
- const type = factoryForSavedObjectType.savedObjectMetaData?.getSavedObjectSubType
- ? factoryForSavedObjectType.savedObjectMetaData.getSavedObjectSubType(so)
- : factoryForSavedObjectType.type;
-
- reportUiCounter(appName, METRIC_TYPE.CLICK, `${type}:add`);
- }
- }
-
- private getCreateMenuItems(): ReactElement[] {
- return [...this.props.getAllFactories()]
- .filter(
- // @ts-expect-error ts 4.5 upgrade
- (factory) => factory.isEditable() && !factory.isContainerType && factory.canCreateNew()
- )
- .map((factory) => (
- this.createNewEmbeddable(factory.type)}
- className="embPanel__addItem"
- >
- {capitalize(factory.getDisplayName())}
-
- ));
- }
-
- public render() {
- const SavedObjectFinder = this.props.SavedObjectFinder;
- const metaData = [...this.props.getAllFactories()]
- .filter(
- (embeddableFactory) =>
- Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
- )
- .map(({ savedObjectMetaData }) => savedObjectMetaData);
- const savedObjectsFinder = (
-
- {this.props.showCreateNewMenu ? (
-
- ) : null}
-
- );
-
- return (
- <>
-
-
-
-
-
-
-
- {savedObjectsFinder}
- >
- );
- }
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/index.ts
deleted file mode 100644
index b9b0243237ef..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/index.ts
+++ /dev/null
@@ -1,10 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-export * from './add_panel_action';
-export * from './open_add_panel_flyout';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
deleted file mode 100644
index eb2722dcf986..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
+++ /dev/null
@@ -1,66 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import React from 'react';
-import { NotificationsStart, OverlayRef, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
-import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
-import { EmbeddableStart } from '../../../../../plugin';
-import { IContainer } from '../../../../containers';
-import { AddPanelFlyout } from './add_panel_flyout';
-
-export function openAddPanelFlyout(options: {
- embeddable: IContainer;
- getFactory: EmbeddableStart['getEmbeddableFactory'];
- getAllFactories: EmbeddableStart['getEmbeddableFactories'];
- overlays: OverlayStart;
- notifications: NotificationsStart;
- SavedObjectFinder: React.ComponentType;
- showCreateNewMenu?: boolean;
- reportUiCounter?: UsageCollectionStart['reportUiCounter'];
- theme: ThemeServiceStart;
- onAddPanel?: (id: string) => void;
-}): OverlayRef {
- const {
- embeddable,
- getFactory,
- getAllFactories,
- overlays,
- notifications,
- SavedObjectFinder,
- showCreateNewMenu,
- reportUiCounter,
- theme,
- onAddPanel,
- } = options;
- const flyoutSession = overlays.openFlyout(
- toMountPoint(
- {
- if (flyoutSession) {
- flyoutSession.close();
- }
- }}
- getFactory={getFactory}
- getAllFactories={getAllFactories}
- notifications={notifications}
- reportUiCounter={reportUiCounter}
- SavedObjectFinder={SavedObjectFinder}
- showCreateNewMenu={showCreateNewMenu}
- />,
- { theme$: theme.theme$ }
- ),
- {
- 'data-test-subj': 'dashboardAddPanel',
- ownFocus: true,
- }
- );
- return flyoutSession;
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx
deleted file mode 100644
index 26bca1c4f3df..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx
+++ /dev/null
@@ -1,52 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import React, { ReactElement, useState } from 'react';
-import { EuiButton } from '@elastic/eui';
-import { EuiContextMenuPanel } from '@elastic/eui';
-import { EuiPopover } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
-
-interface Props {
- menuItems: ReactElement[];
-}
-
-export function SavedObjectFinderCreateNew({ menuItems }: Props) {
- const [isCreateMenuOpen, setCreateMenuOpen] = useState(false);
- const toggleCreateMenu = () => {
- setCreateMenuOpen(!isCreateMenuOpen);
- };
- const closeCreateMenu = () => {
- setCreateMenuOpen(false);
- };
- return (
-
-
-
- }
- isOpen={isCreateMenuOpen}
- closePopover={closeCreateMenu}
- panelPaddingSize="none"
- anchorPosition="downRight"
- >
-
-
- );
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.tsx
deleted file mode 100644
index 563d5c11f5e5..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.tsx
+++ /dev/null
@@ -1,77 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import React from 'react';
-import { SavedObjectFinderCreateNew } from '../saved_object_finder_create_new';
-import { shallow } from 'enzyme';
-import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
-import { mountWithIntl } from '@kbn/test-jest-helpers';
-
-describe('SavedObjectFinderCreateNew', () => {
- test('renders correctly with no items', () => {
- const wrapper = shallow();
- expect(wrapper.find(EuiPopover).length).toEqual(1);
- const menuPanel = wrapper.find(EuiContextMenuPanel);
- expect(menuPanel.length).toEqual(1);
- const panelItems = menuPanel.prop('items');
- if (panelItems) {
- expect(panelItems.length).toEqual(0);
- } else {
- fail('Expect paneltems to be defined');
- }
- });
-
- test('renders correctly with items', () => {
- const items = [];
- const onClick = jest.fn();
- for (let i = 0; i < 3; i++) {
- items.push(
- {`item${
- i + 1
- }`}
- );
- }
-
- const wrapper = shallow();
- expect(wrapper.find(EuiPopover).length).toEqual(1);
- const menuPanel = wrapper.find(EuiContextMenuPanel);
- expect(menuPanel.length).toEqual(1);
- const paneltems = menuPanel.prop('items');
- if (paneltems) {
- expect(paneltems.length).toEqual(3);
- expect(paneltems[0].key).toEqual('1');
- expect(paneltems[1].key).toEqual('2');
- expect(paneltems[2].key).toEqual('3');
- } else {
- fail('Expect paneltems to be defined');
- }
- });
-
- test('clicking the button opens/closes the popover', () => {
- const items = [];
- const onClick = jest.fn();
- for (let i = 0; i < 3; i++) {
- items.push(
- {`item${
- i + 1
- }`}
- );
- }
-
- const component = mountWithIntl();
- let popover = component.find(EuiPopover);
- expect(popover.prop('isOpen')).toBe(false);
- const button = component.find('button');
- button.simulate('click');
- popover = component.find(EuiPopover);
- expect(popover.prop('isOpen')).toBe(true);
- button.simulate('click');
- popover = component.find(EuiPopover);
- expect(popover.prop('isOpen')).toBe(false);
- });
-});
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts
deleted file mode 100644
index c3b8f0d75ef1..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts
+++ /dev/null
@@ -1,12 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-export * from './inspect_panel_action';
-export * from './add_panel';
-export * from './remove_panel_action';
-export * from './customize_panel';
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
deleted file mode 100644
index 00fa8db54552..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
+++ /dev/null
@@ -1,228 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import {
- EuiContextMenuPanelDescriptor,
- EuiBadge,
- EuiIcon,
- EuiToolTip,
- EuiScreenReaderOnly,
- EuiNotificationBadge,
- EuiLink,
-} from '@elastic/eui';
-import classNames from 'classnames';
-import React from 'react';
-import { Action } from '@kbn/ui-actions-plugin/public';
-import { PanelOptionsMenu } from './panel_options_menu';
-import { IEmbeddable } from '../../embeddables';
-import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers';
-import { CustomizePanelAction } from '.';
-
-export interface PanelHeaderProps {
- title?: string;
- description?: string;
- index?: number;
- isViewMode: boolean;
- hidePanelTitle: boolean;
- getActionContextMenuPanel: () => Promise<{
- panels: EuiContextMenuPanelDescriptor[];
- actions: Action[];
- }>;
- closeContextMenu: boolean;
- badges: Array>;
- notifications: Array>;
- embeddable: IEmbeddable;
- headerId?: string;
- showPlaceholderTitle?: boolean;
- customizePanel?: CustomizePanelAction;
-}
-
-function renderBadges(badges: Array>, embeddable: IEmbeddable) {
- return badges.map((badge) => (
- badge.execute({ embeddable, trigger: panelBadgeTrigger })}
- onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
- data-test-subj={`embeddablePanelBadge-${badge.id}`}
- >
- {badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
-
- ));
-}
-
-function renderNotifications(
- notifications: Array>,
- embeddable: IEmbeddable
-) {
- return notifications.map((notification) => {
- const context = { embeddable };
-
- let badge = notification.MenuItem ? (
- React.createElement(notification.MenuItem, {
- key: notification.id,
- context: {
- embeddable,
- trigger: panelNotificationTrigger,
- },
- })
- ) : (
- notification.execute({ ...context, trigger: panelNotificationTrigger })}
- >
- {notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })}
-
- );
-
- if (notification.getDisplayNameTooltip) {
- const tooltip = notification.getDisplayNameTooltip({
- ...context,
- trigger: panelNotificationTrigger,
- });
-
- if (tooltip) {
- badge = (
-
- {badge}
-
- );
- }
- }
-
- return badge;
- });
-}
-
-export function PanelHeader({
- title,
- description,
- index,
- isViewMode,
- hidePanelTitle,
- getActionContextMenuPanel,
- closeContextMenu,
- badges,
- notifications,
- embeddable,
- headerId,
- customizePanel,
-}: PanelHeaderProps) {
- const showTitle = !hidePanelTitle && (!isViewMode || title);
- const showPanelBar =
- !isViewMode || badges.length > 0 || notifications.length > 0 || showTitle || description;
- const classes = classNames('embPanel__header', {
- 'embPanel__header--floater': !showPanelBar,
- });
- const placeholderTitle = i18n.translate('embeddableApi.panel.placeholderTitle', {
- defaultMessage: '[No Title]',
- });
-
- const getAriaLabel = () => {
- return (
-
- );
- };
-
- if (!showPanelBar) {
- return (
-
- );
- }
-
- const renderTitle = () => {
- let titleComponent;
- if (showTitle) {
- titleComponent = isViewMode ? (
-
- {title || placeholderTitle}
-
- ) : customizePanel ? (
- customizePanel.execute({ embeddable })}
- >
- {title || placeholderTitle}
-
- ) : null;
- }
- return description ? (
-
-
- {titleComponent}
-
-
- ) : (
- {titleComponent}
- );
- };
-
- const titleClasses = classNames('embPanel__title', { 'embPanel--dragHandle': !isViewMode });
-
- return (
-
-
- {getAriaLabel()}
- {renderTitle()}
- {renderBadges(badges, embeddable)}
-
- {renderNotifications(notifications, embeddable)}
-
-
- );
-}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx
deleted file mode 100644
index 61cdfeb90a9a..000000000000
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx
+++ /dev/null
@@ -1,166 +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 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 or the Server
- * Side Public License, v 1.
- */
-
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-
-import {
- EuiButtonIcon,
- EuiContextMenu,
- EuiContextMenuPanelDescriptor,
- EuiPopover,
-} from '@elastic/eui';
-import { Action } from '@kbn/ui-actions-plugin/public';
-
-export interface PanelOptionsMenuProps {
- getActionContextMenuPanel: () => Promise<{
- panels: EuiContextMenuPanelDescriptor[];
- actions: Action[];
- }>;
- isViewMode: boolean;
- closeContextMenu: boolean;
- title?: string;
- index?: number;
-}
-
-interface State {
- actionContextMenuPanel?: {
- panels: EuiContextMenuPanelDescriptor[];
- actions: Action[];
- };
- isPopoverOpen: boolean;
- showNotification: boolean;
-}
-
-export class PanelOptionsMenu extends React.Component {
- private mounted = false;
- public static getDerivedStateFromProps(props: PanelOptionsMenuProps, state: State) {
- if (props.closeContextMenu) {
- return {
- ...state,
- isPopoverOpen: false,
- };
- } else {
- return state;
- }
- }
-
- constructor(props: PanelOptionsMenuProps) {
- super(props);
- this.state = {
- actionContextMenuPanel: undefined,
- isPopoverOpen: false,
- showNotification: false,
- };
- }
-
- public async componentDidMount() {
- this.mounted = true;
- this.setState({ actionContextMenuPanel: undefined });
- const actionContextMenuPanel = await this.props.getActionContextMenuPanel();
- const showNotification = actionContextMenuPanel.actions.some(
- (action) => action.showNotification
- );
- if (this.mounted) {
- this.setState({ actionContextMenuPanel, showNotification });
- }
- }
-
- public async componentDidUpdate() {
- const actionContextMenuPanel = await this.props.getActionContextMenuPanel();
- const showNotification = actionContextMenuPanel.actions.some(
- (action) => action.showNotification
- );
- if (this.mounted && this.state.showNotification !== showNotification) {
- this.setState({ showNotification });
- }
- }
-
- public componentWillUnmount() {
- this.mounted = false;
- }
-
- public render() {
- const { isViewMode, title, index } = this.props;
- const enhancedAriaLabel = i18n.translate(
- 'embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel',
- {
- defaultMessage: 'Panel options for {title}',
- values: { title },
- }
- );
- const ariaLabelWithoutTitle =
- index === undefined
- ? i18n.translate('embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel', {
- defaultMessage: 'Panel options',
- })
- : i18n.translate('embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabelWithIndex', {
- defaultMessage: 'Options for panel {index}',
- values: { index },
- });
-
- const button = (
-
- );
-
- return (
-
-
-
- );
- }
- private closePopover = () => {
- if (this.mounted) {
- this.setState({
- isPopoverOpen: false,
- });
- }
- };
-
- private toggleContextMenu = () => {
- if (!this.mounted) return;
- const after = () => {
- if (!this.state.isPopoverOpen) return;
- this.setState({ actionContextMenuPanel: undefined });
- this.props
- .getActionContextMenuPanel()
- .then((actionContextMenuPanel) => {
- if (!this.mounted) return;
- this.setState({ actionContextMenuPanel });
- })
- .catch((error) => console.error(error)); // eslint-disable-line no-console
- };
- this.setState(({ isPopoverOpen }) => ({ isPopoverOpen: !isPopoverOpen }), after);
- };
-}
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
index 317e0d5e741c..b98c61882733 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx
@@ -23,6 +23,11 @@ export class ContactCardEmbeddableFactory
implements EmbeddableFactoryDefinition
{
public readonly type = CONTACT_CARD_EMBEDDABLE;
+ savedObjectMetaData = {
+ name: 'Contact card',
+ type: CONTACT_CARD_EMBEDDABLE,
+ getIconForSavedObject: () => 'document',
+ };
constructor(
protected readonly execTrigger: UiActionsStart['executeTriggerActions'],
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx
index 18dc9778bc3e..50c5d2c2ae17 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx
@@ -33,7 +33,6 @@ interface HelloWorldContainerInput extends ContainerInput {
interface HelloWorldContainerOptions {
getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'];
- panelComponent?: EmbeddableStart['EmbeddablePanel'];
}
export class HelloWorldContainer extends Container {
@@ -41,7 +40,7 @@ export class HelloWorldContainer extends Container,
- private readonly options: HelloWorldContainerOptions,
+ options: HelloWorldContainerOptions,
initializeSettings?: EmbeddableContainerSettings
) {
super(
@@ -64,14 +63,7 @@ export class HelloWorldContainer extends Container
- {this.options.panelComponent ? (
-
- ) : (
- Panel component not provided.
- )}
+
,
node
);
diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx
index fe53cbabbf63..cbf8e09510f3 100644
--- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx
+++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx
@@ -10,12 +10,12 @@ import React, { Component, RefObject } from 'react';
import { Subscription } from 'rxjs';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { IContainer, PanelState, EmbeddableChildPanel } from '../..';
-import { EmbeddableStart } from '../../../plugin';
+
+import { IContainer, PanelState } from '../..';
+import { EmbeddablePanel } from '../../../embeddable_panel';
interface Props {
container: IContainer;
- panelComponent: EmbeddableStart['EmbeddablePanel'];
}
interface State {
@@ -85,10 +85,10 @@ export class HelloWorldContainerComponent extends Component {
const list = Object.values(this.state.panels).map((panelState) => {
const item = (
-
+ this.props.container.untilEmbeddableLoaded(panelState.explicitInput.id)
+ }
/>
);
diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx
index 478845cf66f6..ef5ed38c8257 100644
--- a/src/plugins/embeddable/public/mocks.tsx
+++ b/src/plugins/embeddable/public/mocks.tsx
@@ -6,21 +6,17 @@
* Side Public License, v 1.
*/
-import React from 'react';
-import { coreMock, themeServiceMock } from '@kbn/core/public/mocks';
-import { CoreStart } from '@kbn/core/public';
-import { Start as InspectorStart } from '@kbn/inspector-plugin/public';
-import { type AggregateQuery, type Filter, type Query } from '@kbn/es-query';
-
-import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
-import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import {
SavedObjectManagementTypeInfo,
SavedObjectsManagementPluginStart,
} from '@kbn/saved-objects-management-plugin/public';
+import { coreMock } from '@kbn/core/public/mocks';
+import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks';
+import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
+import { type AggregateQuery, type Filter, type Query } from '@kbn/es-query';
import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
-import { UiActionsService } from './lib/ui_actions';
-import { EmbeddablePublicPlugin } from './plugin';
+import { savedObjectsManagementPluginMock } from '@kbn/saved-objects-management-plugin/public/mocks';
+
import {
EmbeddableStart,
EmbeddableSetup,
@@ -28,58 +24,20 @@ import {
EmbeddableStartDependencies,
EmbeddableStateTransfer,
IEmbeddable,
- EmbeddablePanel,
EmbeddableInput,
SavedObjectEmbeddableInput,
ReferenceOrValueEmbeddable,
SelfStyledEmbeddable,
FilterableEmbeddable,
} from '.';
+import { EmbeddablePublicPlugin } from './plugin';
+import { setKibanaServices } from './kibana_services';
import { SelfStyledOptions } from './lib/self_styled_embeddable/types';
export { mockAttributeService } from './lib/attribute_service/attribute_service.mock';
export type Setup = jest.Mocked;
export type Start = jest.Mocked;
-interface CreateEmbeddablePanelMockArgs {
- getActions: UiActionsService['getTriggerCompatibleActions'];
- getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
- getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
- overlays: CoreStart['overlays'];
- notifications: CoreStart['notifications'];
- application: CoreStart['application'];
- inspector: InspectorStart;
- SavedObjectFinder: React.ComponentType;
-}
-
-const theme = themeServiceMock.createStartContract();
-
-export const createEmbeddablePanelMock = ({
- getActions,
- getEmbeddableFactory,
- getAllEmbeddableFactories,
- overlays,
- notifications,
- application,
- inspector,
- SavedObjectFinder,
-}: Partial) => {
- return ({ embeddable }: { embeddable: IEmbeddable }) => (
- Promise.resolve([]))}
- getAllEmbeddableFactories={getAllEmbeddableFactories || ((() => []) as any)}
- getEmbeddableFactory={getEmbeddableFactory || ((() => undefined) as any)}
- notifications={notifications || ({} as any)}
- application={application || ({} as any)}
- overlays={overlays || ({} as any)}
- inspector={inspector || ({} as any)}
- SavedObjectFinder={SavedObjectFinder || (() => null)}
- theme={theme}
- />
- );
-};
-
export const createEmbeddableStateTransferMock = (): Partial => {
return {
clearEditorState: jest.fn(),
@@ -149,7 +107,6 @@ const createStartContract = (): Start => {
extract: jest.fn(),
inject: jest.fn(),
getAllMigrations: jest.fn(),
- EmbeddablePanel: jest.fn(),
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
getAttributeService: jest.fn(),
};
@@ -183,6 +140,7 @@ const createInstance = (setupPlugins: Partial = {})
inspector: inspectorPluginMock.createStartContract(),
savedObjectsManagement:
savedObjectsManagementMock as unknown as SavedObjectsManagementPluginStart,
+ usageCollection: { reportUiCounter: jest.fn() },
});
return {
plugin,
@@ -199,3 +157,15 @@ export const embeddablePluginMock = {
mockSelfStyledEmbeddable,
mockFilterableEmbeddable,
};
+
+export const setStubKibanaServices = () => {
+ const core = coreMock.createStart();
+ const selfStart = embeddablePluginMock.createStartContract();
+
+ setKibanaServices(core, selfStart, {
+ uiActions: uiActionsPluginMock.createStartContract(),
+ inspector: inspectorPluginMock.createStartContract(),
+ savedObjectsManagement: savedObjectsManagementPluginMock.createStartContract(),
+ usageCollection: { reportUiCounter: jest.fn() },
+ });
+};
diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx
index 396ee5f6db8b..df18c8e47245 100644
--- a/src/plugins/embeddable/public/plugin.tsx
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -6,12 +6,10 @@
* Side Public License, v 1.
*/
-import React from 'react';
import { Subscription } from 'rxjs';
import { identity } from 'lodash';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import type { SerializableRecord } from '@kbn/utility-types';
-import { getSavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { Start as InspectorStart } from '@kbn/inspector-plugin/public';
import {
@@ -22,6 +20,7 @@ import {
PublicAppInfo,
} from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
+import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { migrateToLatest, PersistableStateService } from '@kbn/kibana-utils-plugin/common';
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
@@ -39,9 +38,7 @@ import {
EmbeddableOutput,
defaultEmbeddableFactoryProvider,
IEmbeddable,
- EmbeddablePanel,
SavedObjectEmbeddableInput,
- EmbeddableContainerContext,
PANEL_BADGE_TRIGGER,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
@@ -57,7 +54,8 @@ import {
} from '../common/lib';
import { getAllMigrations } from '../common/lib/get_all_migrations';
import { setTheme } from './services';
-import { CustomTimeRangeBadge } from './lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge';
+import { setKibanaServices } from './kibana_services';
+import { CustomTimeRangeBadge } from './embeddable_panel/panel_actions';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@@ -66,6 +64,7 @@ export interface EmbeddableSetupDependencies {
export interface EmbeddableStartDependencies {
uiActions: UiActionsStart;
inspector: InspectorStart;
+ usageCollection: UsageCollectionStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
}
@@ -92,7 +91,6 @@ export interface EmbeddableStart extends PersistableStateService EmbeddableFactory | undefined;
getEmbeddableFactories: () => IterableIterator;
- EmbeddablePanel: EmbeddablePanelHOC;
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
getAttributeService: <
A extends { title: string },
@@ -106,14 +104,6 @@ export interface EmbeddableStart extends PersistableStateService
) => AttributeService;
}
-
-export type EmbeddablePanelHOC = React.FC<{
- embeddable: IEmbeddable;
- hideHeader?: boolean;
- containerContext?: EmbeddableContainerContext;
- index?: number;
-}>;
-
export class EmbeddablePublicPlugin implements Plugin {
private readonly embeddableFactoryDefinitions: Map =
new Map();
@@ -145,15 +135,7 @@ export class EmbeddablePublicPlugin implements Plugin {
this.embeddableFactories.set(
def.type,
@@ -163,6 +145,7 @@ export class EmbeddablePublicPlugin implements Plugin
- ({
- embeddable,
- hideHeader,
- containerContext,
- index,
- }: {
- embeddable: IEmbeddable;
- hideHeader?: boolean;
- containerContext?: EmbeddableContainerContext;
- index?: number;
- }) =>
- (
-
- );
-
const commonContract: CommonEmbeddableStartContract = {
getEmbeddableFactory: this
.getEmbeddableFactory as unknown as CommonEmbeddableStartContract['getEmbeddableFactory'],
@@ -240,7 +184,7 @@ export class EmbeddablePublicPlugin implements Plugin
@@ -254,7 +198,6 @@ export class EmbeddablePublicPlugin implements Plugin {
});
test('Container.removeEmbeddable removes and cleans up', async () => {
- const { start, testPanel } = await createHelloWorldContainerAndEmbeddable();
+ const { start } = await createHelloWorldContainerAndEmbeddable();
const container = new HelloWorldContainer(
{
@@ -254,7 +242,6 @@ test('Container.removeEmbeddable removes and cleans up', async () => {
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
const embeddable = await container.addNewEmbeddable<
@@ -397,14 +384,13 @@ test('Container view mode change propagates to children', async () => {
});
test(`Container updates its state when a child's input is updated`, async () => {
- const { container, embeddable, start, coreStart, uiActions } =
- await createHelloWorldContainerAndEmbeddable(
- { id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
- {
- id: '123',
- firstName: 'Susy',
- }
- );
+ const { container, embeddable, start } = await createHelloWorldContainerAndEmbeddable(
+ { id: 'hello', panels: {}, viewMode: ViewMode.VIEW },
+ {
+ id: '123',
+ firstName: 'Susy',
+ }
+ );
expect(isErrorEmbeddable(embeddable)).toBe(false);
@@ -420,17 +406,8 @@ test(`Container updates its state when a child's input is updated`, async () =>
// Make sure a brand new container built off the output of container also creates an embeddable
// with "Dr.", not the default the embeddable was first added with. Makes sure changed input
// is preserved with the container.
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const containerClone = new HelloWorldContainer(container.getInput(), {
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
});
const cloneSubscription = Rx.merge(
containerClone.getOutput$(),
@@ -668,14 +645,6 @@ test('Container changes made directly after adding a new embeddable are propagat
const start = doStart();
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -684,7 +653,6 @@ test('Container changes made directly after adding a new embeddable are propagat
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
@@ -787,19 +755,8 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
});
test('untilEmbeddableLoaded() throws an error if there is no such child panel in the container - 2', async () => {
- const { doStart, coreStart, uiActions } = testPlugin(
- coreMock.createSetup(),
- coreMock.createStart()
- );
+ const { doStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const start = doStart();
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -807,7 +764,6 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
@@ -817,21 +773,10 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
});
test('untilEmbeddableLoaded() resolves if child is loaded in the container', async () => {
- const { setup, doStart, coreStart, uiActions } = testPlugin(
- coreMock.createSetup(),
- coreMock.createStart()
- );
+ const { setup, doStart } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const factory = new HelloWorldEmbeddableFactoryDefinition();
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -844,7 +789,6 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
@@ -854,10 +798,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
});
test('untilEmbeddableLoaded resolves with undefined if child is subsequently removed', async () => {
- const { doStart, setup, coreStart, uiActions } = testPlugin(
- coreMock.createSetup(),
- coreMock.createStart()
- );
+ const { doStart, setup, uiActions } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 3,
execAction: uiActions.executeTriggerActions,
@@ -865,14 +806,6 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -885,7 +818,6 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
@@ -897,24 +829,13 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
});
test('adding a panel then subsequently removing it before its loaded removes the panel', (done) => {
- const { doStart, coreStart, uiActions, setup } = testPlugin(
- coreMock.createSetup(),
- coreMock.createStart()
- );
+ const { doStart, uiActions, setup } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const factory = new SlowContactCardEmbeddableFactory({
loadTickCount: 1,
execAction: uiActions.executeTriggerActions,
});
setup.registerEmbeddableFactory(factory.type, factory);
const start = doStart();
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -927,7 +848,6 @@ test('adding a panel then subsequently removing it before its loaded removes the
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
diff --git a/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx
index 4a4e7733ba40..58dc2eb9049c 100644
--- a/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx
+++ b/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx
@@ -11,7 +11,6 @@ import * as React from 'react';
import { EmbeddableOutput, isErrorEmbeddable, ViewMode } from '../lib';
import { coreMock } from '@kbn/core/public/mocks';
import { testPlugin } from './test_plugin';
-import { CustomizePanelEditor } from '../lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import {
EmbeddableTimeRangeInput,
@@ -20,6 +19,7 @@ import {
TimeRangeEmbeddableFactory,
TIME_RANGE_EMBEDDABLE,
} from '../lib/test_samples';
+import { CustomizePanelEditor } from '../embeddable_panel/panel_actions/customize_panel_action/customize_panel_editor';
let container: TimeRangeContainer;
let embeddable: TimeRangeEmbeddable;
diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts
index 4ed4c12a8039..f278049444d0 100644
--- a/src/plugins/embeddable/public/tests/explicit_input.test.ts
+++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts
@@ -21,12 +21,8 @@ import { FilterableContainer } from '../lib/test_samples/embeddables/filterable_
import { isErrorEmbeddable } from '../lib';
import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container';
import { coreMock } from '@kbn/core/public/mocks';
-import { createEmbeddablePanelMock } from '../mocks';
-const { setup, doStart, coreStart, uiActions } = testPlugin(
- coreMock.createSetup(),
- coreMock.createStart()
-);
+const { setup, doStart, uiActions } = testPlugin(coreMock.createSetup(), coreMock.createStart());
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const factory = new SlowContactCardEmbeddableFactory({
@@ -69,19 +65,10 @@ test('Explicit embeddable input mapped to undefined will default to inherited',
});
test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async () => {
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{ id: 'hello', panels: {} },
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
@@ -111,14 +98,6 @@ test('Explicit embeddable input mapped to undefined with no inherited value will
// but before the embeddable factory returns the embeddable, that the `inheritedChildInput` and
// embeddable input comparisons won't cause explicit input to be set when it shouldn't.
test('Explicit input tests in async situations', (done: () => void) => {
- const testPanel = createEmbeddablePanelMock({
- getActions: uiActions.getTriggerCompatibleActions,
- getEmbeddableFactory: start.getEmbeddableFactory,
- getAllEmbeddableFactories: start.getEmbeddableFactories,
- overlays: coreStart.overlays,
- notifications: coreStart.notifications,
- application: coreStart.application,
- });
const container = new HelloWorldContainer(
{
id: 'hello',
@@ -131,7 +110,6 @@ test('Explicit input tests in async situations', (done: () => void) => {
},
{
getEmbeddableFactory: start.getEmbeddableFactory,
- panelComponent: testPanel,
}
);
diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts
index 09412d973ec7..75792ab5fde3 100644
--- a/src/plugins/embeddable/public/tests/test_plugin.ts
+++ b/src/plugins/embeddable/public/tests/test_plugin.ts
@@ -65,6 +65,7 @@ export const testPlugin = (
uiActions: uiActionsPluginMock.createStartContract(),
savedObjectsManagement:
savedObjectsManagementMock as unknown as SavedObjectsManagementPluginStart,
+ usageCollection: { reportUiCounter: jest.fn() },
});
return start;
},
diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json
index 2c1cfcf4ed6c..8962fec29daa 100644
--- a/src/plugins/embeddable/tsconfig.json
+++ b/src/plugins/embeddable/tsconfig.json
@@ -1,14 +1,9 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
- "outDir": "target/types",
+ "outDir": "target/types"
},
- "include": [
- ".storybook/**/*",
- "common/**/*",
- "public/**/*",
- "server/**/*"
- ],
+ "include": ["*.ts", ".storybook/**/*", "common/**/*", "public/**/*", "server/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/inspector-plugin",
@@ -25,17 +20,15 @@
"@kbn/test-jest-helpers",
"@kbn/std",
"@kbn/expressions-plugin",
- "@kbn/usage-collection-plugin",
- "@kbn/analytics",
"@kbn/data-plugin",
"@kbn/core-overlays-browser-mocks",
"@kbn/core-theme-browser-mocks",
"@kbn/saved-objects-management-plugin",
"@kbn/saved-objects-tagging-oss-plugin",
"@kbn/saved-objects-finder-plugin",
- "@kbn/ui-actions-browser",
+ "@kbn/analytics",
+ "@kbn/usage-collection-plugin",
+ "@kbn/ui-theme"
],
- "exclude": [
- "target/**/*",
- ]
+ "exclude": ["target/**/*"]
}
diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts
index 7b3e48151fd1..ea381db06821 100644
--- a/test/examples/embeddables/adding_children.ts
+++ b/test/examples/embeddables/adding_children.ts
@@ -41,8 +41,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
});
it('Can add a child backed off a saved object', async () => {
- await testSubjects.click('embeddablePanelToggleMenuIcon');
- await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL');
+ await testSubjects.click('addPanelToListContainer');
await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator');
await toggleFilterPopover();
await clickFilter('Todo');
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
index fb8f34be23cc..c99bfd21c9ad 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
@@ -15,6 +15,7 @@ import {
EmbeddableFactory,
EmbeddableFactoryNotFoundError,
isErrorEmbeddable,
+ EmbeddablePanel,
} from '@kbn/embeddable-plugin/public';
import type { EmbeddableContainerContext } from '@kbn/embeddable-plugin/public';
import { StartDeps } from '../../plugin';
@@ -50,10 +51,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
};
return (
-
+
);
};
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
index 67076fb9c920..0377765ef04a 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, useEffect } from 'react';
-import type { CoreStart, ThemeServiceStart } from '@kbn/core/public';
+import type { CoreStart } from '@kbn/core/public';
import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
import { EuiLoadingChart } from '@elastic/eui';
@@ -93,9 +93,8 @@ interface PluginsStartDependencies {
}
export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) {
- const { embeddable: embeddableStart, uiActions, inspector } = plugins;
+ const { embeddable: embeddableStart, uiActions } = plugins;
const factory = embeddableStart.getEmbeddableFactory('lens')!;
- const theme = core.theme;
return (props: EmbeddableComponentProps) => {
const input = { ...props };
const hasActions =
@@ -106,10 +105,8 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep
hasActions}
input={input}
- theme={theme}
extraActions={input.extraActions}
showInspector={input.showInspector}
withDefaultActions={input.withDefaultActions}
@@ -137,10 +134,8 @@ function EmbeddableRootWrapper({
interface EmbeddablePanelWrapperProps {
factory: EmbeddableFactory;
uiActions: PluginsStartDependencies['uiActions'];
- inspector: PluginsStartDependencies['inspector'];
actionPredicate: (id: string) => boolean;
input: EmbeddableComponentProps;
- theme: ThemeServiceStart;
extraActions?: Action[];
showInspector?: boolean;
withDefaultActions?: boolean;
@@ -150,9 +145,7 @@ const EmbeddablePanelWrapper: FC = ({
factory,
uiActions,
actionPredicate,
- inspector,
input,
- theme,
extraActions,
showInspector = true,
withDefaultActions,
@@ -179,12 +172,11 @@ const EmbeddablePanelWrapper: FC = ({
return [...(extraActions ?? []), ...actions];
}}
- inspector={showInspector ? inspector : undefined}
+ hideInspector={!showInspector}
actionPredicate={actionPredicate}
+ showNotifications={false}
showShadow={false}
showBadges={false}
- showNotifications={false}
- theme={theme}
/>
);
};
diff --git a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx
index a4a709b9e6f1..d7f4c6f0573d 100644
--- a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.test.tsx
@@ -19,6 +19,8 @@ import { getLayerList } from './map_config';
import { useIsFieldInIndexPattern } from '../../../containers/fields';
import { buildTimeRangeFilter } from '../../../../detections/components/alerts_table/helpers';
+import { setStubKibanaServices } from '@kbn/embeddable-plugin/public/mocks';
+
jest.mock('./create_embeddable');
jest.mock('./map_config');
jest.mock('../../../../common/containers/sourcerer');
@@ -29,9 +31,6 @@ jest.mock('./index_patterns_missing_prompt', () => ({
jest.mock('../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
- embeddable: {
- EmbeddablePanel: jest.fn(() => ),
- },
docLinks: {
ELASTIC_WEBSITE_URL: 'ELASTIC_WEBSITE_URL',
links: {
@@ -51,6 +50,10 @@ jest.mock('../../../../common/lib/kibana', () => ({
remove: jest.fn(),
}),
}));
+jest.mock('@kbn/embeddable-plugin/public', () => ({
+ ...jest.requireActual('@kbn/embeddable-plugin/public'),
+ EmbeddablePanel: jest.fn().mockReturnValue(),
+}));
const mockUseSourcererDataView = useSourcererDataView as jest.Mock;
const mockCreateEmbeddable = createEmbeddable as jest.Mock;
@@ -107,6 +110,9 @@ describe('EmbeddedMapComponent', () => {
mockUseSourcererDataView.mockReturnValue({ selectedPatterns: ['filebeat-*', 'auditbeat-*'] });
mockCreateEmbeddable.mockResolvedValue(embeddableValue);
mockUseIsFieldInIndexPattern.mockReturnValue(() => true);
+
+ // stub Kibana services for the embeddable plugin to ensure embeddable panel renders.
+ setStubKibanaServices();
});
afterEach(() => {
@@ -138,7 +144,7 @@ describe('EmbeddedMapComponent', () => {
});
});
- test('renders services.embeddable.EmbeddablePanel', async () => {
+ test('renders EmbeddablePanel from embeddable plugin', async () => {
const { getByTestId, queryByTestId } = render(
diff --git a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx
index e7869b3be613..d20fd4d1fb49 100644
--- a/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/components/embeddables/embedded_map.tsx
@@ -12,8 +12,11 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { createHtmlPortalNode, InPortal } from 'react-reverse-portal';
import styled, { css } from 'styled-components';
import type { Filter, Query } from '@kbn/es-query';
-import type { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
-import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
+import {
+ EmbeddablePanel,
+ isErrorEmbeddable,
+ type ErrorEmbeddable,
+} from '@kbn/embeddable-plugin/public';
import type { MapEmbeddable } from '@kbn/maps-plugin/public/embeddable';
import { isEqual } from 'lodash/fp';
import { buildTimeRangeFilter } from '../../../../detections/components/alerts_table/helpers';
@@ -279,14 +282,14 @@ export const EmbeddedMapComponent = ({
{isIndexError ? (
) : embeddable != null ? (
-
+
) : (
)}
);
- }, [embeddable, isIndexError, portalNode, services, storageValue]);
+ }, [embeddable, isIndexError, portalNode, storageValue]);
return isError ? null : (