From a1be0337346e67b044ab41d306275bc639bfa8ae Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 17 Jul 2023 12:14:31 -0400 Subject: [PATCH] [Embeddable] Refactor embeddable panel (#159837) Update the Embeddable panel and all sub-components to be react function components & removes the embeddable panel HOC in favour of a direct import. --- .../public/list_container/list_container.tsx | 7 +- .../list_container_component.tsx | 23 +- .../searchable_list_container_component.tsx | 19 +- examples/embeddable_explorer/public/app.tsx | 1 - .../public/embeddable_panel_example.tsx | 7 +- .../component/grid/dashboard_grid_item.tsx | 17 +- .../embeddable/api/add_panel_from_library.ts | 29 +- .../embeddable/dashboard_container.test.tsx | 92 +- .../diffing/dashboard_diffing_functions.ts | 8 +- .../services/embeddable/embeddable.stub.ts | 1 - .../services/embeddable/embeddable_service.ts | 1 - .../public/services/embeddable/types.ts | 1 - src/plugins/embeddable/README.md | 1 - src/plugins/embeddable/jest.config.js | 1 + .../lib/panel/index.ts => jest_setup.ts} | 4 +- src/plugins/embeddable/kibana.jsonc | 12 +- .../__stories__/embeddable_panel.stories.tsx | 1 - .../add_panel_flyout.test.tsx | 119 +++ .../add_panel_flyout/add_panel_flyout.tsx | 143 ++++ .../open_add_panel_flyout.tsx | 43 + .../panel_options_menu.stories.tsx | 50 -- .../components/panel_options_menu/index.tsx | 79 -- .../_embeddable_panel.scss | 4 + .../embeddable_loading_indicator.tsx | 24 + .../embeddable_panel.test.tsx | 516 ++++++++++++ .../embeddable_panel/embeddable_panel.tsx | 221 +++++ .../embeddable_panel_error.tsx | 12 +- .../embeddable_panel_strings.ts | 49 ++ .../public/embeddable_panel/index.tsx | 23 + .../can_inherit_time_range.test.ts | 4 +- .../can_inherit_time_range.ts | 3 +- .../custom_time_range_badge.test.ts | 5 +- .../custom_time_range_badge.tsx | 5 +- .../customize_panel_action.test.ts | 10 +- .../customize_panel_action.tsx | 15 +- .../customize_panel_editor.tsx | 7 +- .../does_inherit_time_range.ts | 2 +- .../customize_panel_action}/index.ts | 1 + .../edit_panel_action.test.tsx | 13 +- .../edit_panel_action}/edit_panel_action.ts | 16 +- .../embeddable_panel/panel_actions/index.ts | 19 + .../inspect_panel_action.test.tsx | 15 +- .../inspect_panel_action.ts | 3 +- .../remove_panel_action.test.tsx | 17 +- .../remove_panel_action.ts | 7 +- .../embeddable_panel_context_menu.tsx | 171 ++++ .../panel_header/embeddable_panel_header.tsx | 113 +++ .../panel_header/embeddable_panel_title.tsx | 75 ++ .../use_embeddable_panel_badges.tsx | 165 ++++ .../public/embeddable_panel/types.ts | 86 ++ .../use_embeddable_panel.test.ts | 62 ++ .../embeddable_panel/use_embeddable_panel.ts | 45 + .../use_select_from_embeddable.ts | 49 ++ src/plugins/embeddable/public/index.scss | 3 +- src/plugins/embeddable/public/index.ts | 32 +- .../embeddable/public/kibana_services.ts | 50 ++ .../embeddable/public/lib/actions/index.ts | 9 - .../embeddable_child_panel.test.tsx | 97 --- .../lib/containers/embeddable_child_panel.tsx | 140 ---- .../embeddable/public/lib/containers/index.ts | 1 - .../lib/embeddables/error_embeddable.tsx | 6 +- src/plugins/embeddable/public/lib/index.ts | 2 - .../embeddable/public/lib/panel/_index.scss | 1 - .../lib/panel/embeddable_panel.test.tsx | 787 ------------------ .../public/lib/panel/embeddable_panel.tsx | 476 ----------- .../public/lib/panel/panel_header/_index.scss | 1 - .../_panel_options_menu_form.scss | 3 - .../public/lib/panel/panel_header/index.ts | 9 - .../add_panel/add_panel_action.test.tsx | 125 --- .../add_panel/add_panel_action.ts | 70 -- .../add_panel/add_panel_flyout.test.tsx | 129 --- .../add_panel/add_panel_flyout.tsx | 191 ----- .../panel_actions/add_panel/index.ts | 10 - .../add_panel/open_add_panel_flyout.tsx | 66 -- .../saved_object_finder_create_new.tsx | 52 -- .../saved_object_finder_create_new.test.tsx | 77 -- .../panel/panel_header/panel_actions/index.ts | 12 - .../lib/panel/panel_header/panel_header.tsx | 228 ----- .../panel/panel_header/panel_options_menu.tsx | 166 ---- .../contact_card_embeddable_factory.tsx | 5 + .../embeddables/hello_world_container.tsx | 12 +- .../hello_world_container_component.tsx | 14 +- src/plugins/embeddable/public/mocks.tsx | 72 +- src/plugins/embeddable/public/plugin.tsx | 74 +- .../embeddable/public/tests/container.test.ts | 104 +-- .../tests/customize_panel_editor.test.tsx | 2 +- .../public/tests/explicit_input.test.ts | 24 +- .../embeddable/public/tests/test_plugin.ts | 1 + src/plugins/embeddable/tsconfig.json | 19 +- test/examples/embeddables/adding_children.ts | 3 +- .../renderers/embeddable/embeddable.tsx | 6 +- .../embeddable/embeddable_component.tsx | 16 +- .../embeddables/embedded_map.test.tsx | 14 +- .../components/embeddables/embedded_map.tsx | 11 +- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 97 files changed, 2255 insertions(+), 3296 deletions(-) rename src/plugins/embeddable/{public/lib/panel/index.ts => jest_setup.ts} (81%) create mode 100644 src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.test.tsx create mode 100644 src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx create mode 100644 src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx delete mode 100644 src/plugins/embeddable/public/components/panel_options_menu/__examples__/panel_options_menu.stories.tsx delete mode 100644 src/plugins/embeddable/public/components/panel_options_menu/index.tsx rename src/plugins/embeddable/public/{lib/panel => embeddable_panel}/_embeddable_panel.scss (98%) create mode 100644 src/plugins/embeddable/public/embeddable_panel/embeddable_loading_indicator.tsx create mode 100644 src/plugins/embeddable/public/embeddable_panel/embeddable_panel.test.tsx create mode 100644 src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx rename src/plugins/embeddable/public/{lib/panel => embeddable_panel}/embeddable_panel_error.tsx (97%) create mode 100644 src/plugins/embeddable/public/embeddable_panel/embeddable_panel_strings.ts create mode 100644 src/plugins/embeddable/public/embeddable_panel/index.tsx rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/can_inherit_time_range.test.ts (93%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/can_inherit_time_range.ts (99%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/custom_time_range_badge.test.ts (98%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/custom_time_range_badge.tsx (97%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/customize_panel_action.test.ts (84%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/customize_panel_action.tsx (96%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/customize_panel_editor.tsx (99%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/does_inherit_time_range.ts (99%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions/customize_panel => embeddable_panel/panel_actions/customize_panel_action}/index.ts (90%) rename src/plugins/embeddable/public/{lib/actions => embeddable_panel/panel_actions/edit_panel_action}/edit_panel_action.test.tsx (94%) rename src/plugins/embeddable/public/{lib/actions => embeddable_panel/panel_actions/edit_panel_action}/edit_panel_action.ts (96%) create mode 100644 src/plugins/embeddable/public/embeddable_panel/panel_actions/index.ts rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions => embeddable_panel/panel_actions/inspect_panel_action}/inspect_panel_action.test.tsx (95%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions => embeddable_panel/panel_actions/inspect_panel_action}/inspect_panel_action.ts (97%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions => embeddable_panel/panel_actions/remove_panel_action}/remove_panel_action.test.tsx (83%) rename src/plugins/embeddable/public/{lib/panel/panel_header/panel_actions => embeddable_panel/panel_actions/remove_panel_action}/remove_panel_action.ts (90%) create mode 100644 src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_context_menu.tsx create mode 100644 src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_header.tsx create mode 100644 src/plugins/embeddable/public/embeddable_panel/panel_header/embeddable_panel_title.tsx create mode 100644 src/plugins/embeddable/public/embeddable_panel/panel_header/use_embeddable_panel_badges.tsx create mode 100644 src/plugins/embeddable/public/embeddable_panel/types.ts create mode 100644 src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.test.ts create mode 100644 src/plugins/embeddable/public/embeddable_panel/use_embeddable_panel.ts create mode 100644 src/plugins/embeddable/public/embeddable_panel/use_select_from_embeddable.ts create mode 100644 src/plugins/embeddable/public/kibana_services.ts delete mode 100644 src/plugins/embeddable/public/lib/actions/index.ts delete mode 100644 src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx delete mode 100644 src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/_index.scss delete mode 100644 src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/_index.scss delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/_panel_options_menu_form.scss delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/index.ts delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/index.ts delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/saved_object_finder_create_new.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/tests/saved_object_finder_create_new.test.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx delete mode 100644 src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx 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 = ( + + {ariaLabel} + + ); + + 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 ( - - {showPanelBar && title - ? i18n.translate('embeddableApi.panel.enhancedDashboardPanelAriaLabel', { - defaultMessage: 'Dashboard panel: {title}', - values: { title: title || placeholderTitle }, - }) - : i18n.translate('embeddableApi.panel.dashboardPanelAriaLabel', { - defaultMessage: 'Dashboard panel', - })} - - ); - }; - - if (!showPanelBar) { - return ( -
- - {getAriaLabel()} -
- ); - } - - 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 : (