mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[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.
This commit is contained in:
parent
48ec52b202
commit
a1be033734
97 changed files with 2255 additions and 3296 deletions
|
@ -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(
|
||||
<ListContainerComponent embeddable={this} embeddableServices={this.embeddableServices} />,
|
||||
node
|
||||
);
|
||||
ReactDOM.render(<ListContainerComponent embeddable={this} />, node);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
|
|
|
@ -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(
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EmbeddableChildPanel
|
||||
PanelComponent={embeddableServices.EmbeddablePanel}
|
||||
embeddableId={panel.explicitInput.id}
|
||||
container={embeddable}
|
||||
<EmbeddablePanel
|
||||
embeddable={() => embeddable.untilEmbeddableLoaded(panel.explicitInput.id)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -55,12 +47,12 @@ function renderList(
|
|||
return list;
|
||||
}
|
||||
|
||||
export function ListContainerComponentInner({ embeddable, input, embeddableServices }: Props) {
|
||||
export function ListContainerComponentInner({ embeddable, input }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<h2 data-test-subj="listContainerTitle">{embeddable.getTitle()}</h2>
|
||||
<EuiSpacer size="l" />
|
||||
{renderList(embeddable, input.panels, embeddableServices)}
|
||||
{renderList(embeddable, input.panels)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -73,6 +65,5 @@ export function ListContainerComponentInner({ embeddable, input, embeddableServi
|
|||
export const ListContainerComponent = withEmbeddableSubscription<
|
||||
ContainerInput,
|
||||
ContainerOutput,
|
||||
IContainer,
|
||||
{ embeddableServices: EmbeddableStart }
|
||||
IContainer
|
||||
>(ListContainerComponentInner);
|
||||
|
|
|
@ -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<Props, Stat
|
|||
};
|
||||
|
||||
public renderControls() {
|
||||
const { input } = this.props;
|
||||
const { input, embeddable } = this.props;
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -150,6 +152,17 @@ export class SearchableListContainerComponentInner extends Component<Props, Stat
|
|||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiButton
|
||||
data-test-subj="addPanelToListContainer"
|
||||
disabled={input.search === ''}
|
||||
onClick={() => openAddPanelFlyout({ container: embeddable })}
|
||||
>
|
||||
Add panel
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
@ -171,7 +184,7 @@ export class SearchableListContainerComponentInner extends Component<Props, Stat
|
|||
}
|
||||
|
||||
private renderList() {
|
||||
const { embeddableServices, input, embeddable } = this.props;
|
||||
const { input, embeddable } = this.props;
|
||||
let id = 0;
|
||||
const list = Object.values(input.panels).map((panel) => {
|
||||
const childEmbeddable = embeddable.getChild(panel.explicitInput.id);
|
||||
|
@ -189,7 +202,7 @@ export class SearchableListContainerComponentInner extends Component<Props, Stat
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<embeddableServices.EmbeddablePanel embeddable={childEmbeddable} />
|
||||
<EmbeddablePanel embeddable={childEmbeddable} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -112,7 +112,6 @@ const EmbeddableExplorerApp = ({
|
|||
id: 'embeddablePanelExample',
|
||||
component: (
|
||||
<EmbeddablePanelExample
|
||||
embeddableServices={embeddableApi}
|
||||
searchListContainerFactory={embeddableExamples.factories.getSearchableListContainerEmbeddableFactory()}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -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
|
|||
</EuiText>
|
||||
<EuiPanel data-test-subj="embeddedPanelExample" paddingSize="none" role="figure">
|
||||
{embeddable ? (
|
||||
<embeddableServices.EmbeddablePanel embeddable={embeddable} />
|
||||
<EmbeddablePanel embeddable={embeddable} />
|
||||
) : (
|
||||
<EuiText>Loading...</EuiText>
|
||||
)}
|
||||
|
|
|
@ -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<HTMLDivElement, Props>(
|
|||
},
|
||||
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<HTMLDivElement, Props>(
|
|||
>
|
||||
{isRenderable ? (
|
||||
<>
|
||||
<EmbeddableChildPanel
|
||||
// This key is used to force rerendering on embeddable type change while the id remains the same
|
||||
<EmbeddablePanel
|
||||
key={type}
|
||||
embeddableId={id}
|
||||
index={index}
|
||||
showBadges={true}
|
||||
showNotifications={true}
|
||||
onPanelStatusChange={onPanelStatusChange}
|
||||
{...{ container, PanelComponent }}
|
||||
embeddable={() => container.untilEmbeddableLoaded(id)}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
<I18nProvider>
|
||||
<DashboardServicesProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={(() => []) as any}
|
||||
getEmbeddableFactory={(() => null) as any}
|
||||
notifications={{} as any}
|
||||
application={application}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</DashboardServicesProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = await mount(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel embeddable={embeddable} />
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
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();
|
||||
|
|
|
@ -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<boolean>((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
|
||||
),
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -24,7 +24,6 @@ export const embeddableServiceFactory: EmbeddableServiceFactory = ({ startPlugin
|
|||
'getEmbeddableFactory',
|
||||
'getEmbeddableFactories',
|
||||
'getStateTransfer',
|
||||
'EmbeddablePanel',
|
||||
'getAllMigrations',
|
||||
'telemetry',
|
||||
'extract',
|
||||
|
|
|
@ -14,7 +14,6 @@ export type DashboardEmbeddableService = Pick<
|
|||
| 'getEmbeddableFactory'
|
||||
| 'getAllMigrations'
|
||||
| 'getStateTransfer'
|
||||
| 'EmbeddablePanel'
|
||||
| 'telemetry'
|
||||
| 'extract'
|
||||
| 'inject'
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -12,5 +12,6 @@ module.exports = {
|
|||
roots: ['<rootDir>/src/plugins/embeddable'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/embeddable',
|
||||
coverageReporters: ['text', 'html'],
|
||||
setupFiles: ['<rootDir>/src/plugins/embeddable/jest_setup.ts'],
|
||||
collectCoverageFrom: ['<rootDir>/src/plugins/embeddable/{common,public,server}/**/*.{ts,tsx}'],
|
||||
};
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './embeddable_panel';
|
||||
export * from './panel_header';
|
||||
import { setStubKibanaServices } from './public/mocks';
|
||||
setStubKibanaServices();
|
|
@ -14,14 +14,8 @@
|
|||
"savedObjectsFinder",
|
||||
"savedObjectsManagement"
|
||||
],
|
||||
"optionalPlugins": ["savedObjectsTaggingOss"],
|
||||
"requiredBundles": [
|
||||
"savedObjects",
|
||||
"kibanaReact",
|
||||
"kibanaUtils"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"common"
|
||||
]
|
||||
"optionalPlugins": ["savedObjectsTaggingOss", "usageCollection"],
|
||||
"requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"],
|
||||
"extraPublicDirs": ["common"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,6 @@ const HelloWorldEmbeddablePanel = forwardRef<
|
|||
getActions={getActions}
|
||||
hideHeader={hideHeader}
|
||||
showShadow={showShadow}
|
||||
theme={{ theme$ }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<void>;
|
||||
}) => (
|
||||
<button
|
||||
id="soFinderDummyButton"
|
||||
onClick={() =>
|
||||
onChoose?.(
|
||||
'testId',
|
||||
'CONTACT_CARD_EMBEDDABLE',
|
||||
'test name',
|
||||
{} as unknown as SavedObjectCommon<unknown>
|
||||
)
|
||||
}
|
||||
>
|
||||
Dummy Button!
|
||||
</button>
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
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(<AddPanelFlyout container={container} />);
|
||||
|
||||
// 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(<AddPanelFlyout container={container} />);
|
||||
|
||||
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(<AddPanelFlyout container={container} />);
|
||||
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(<AddPanelFlyout container={container} />);
|
||||
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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<unknown>),
|
||||
[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<SavedObjectEmbeddableInput>(
|
||||
factoryForSavedObjectType.type,
|
||||
{ savedObjectId: id }
|
||||
);
|
||||
onAddPanel?.(embeddable.id);
|
||||
|
||||
showSuccessToast(name);
|
||||
runAddTelemetry(container.type, factoryForSavedObjectType, savedObject);
|
||||
},
|
||||
[container, factoriesBySavedObjectType, onAddPanel]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
{i18n.translate('embeddableApi.addPanel.Title', { defaultMessage: 'Add from library' })}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<SavedObjectFinder
|
||||
services={{
|
||||
http: core.http,
|
||||
savedObjectsManagement,
|
||||
uiSettings: core.uiSettings,
|
||||
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
|
||||
}}
|
||||
onChoose={onChoose}
|
||||
savedObjectMetaData={metaData}
|
||||
showFilter={true}
|
||||
noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', {
|
||||
defaultMessage: 'No matching objects found.',
|
||||
})}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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(
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<LazyAddPanelFlyout container={container} onAddPanel={onAddPanel} />
|
||||
</Suspense>,
|
||||
{ theme$: core.theme.theme$ }
|
||||
),
|
||||
{
|
||||
'data-test-subj': 'dashboardAddPanel',
|
||||
ownFocus: true,
|
||||
}
|
||||
);
|
||||
return flyoutSession;
|
||||
};
|
|
@ -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) => (
|
||||
<div style={{ height: 150 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export function Default({ isViewMode }: React.ComponentProps<typeof PanelOptionsMenu>) {
|
||||
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 <PanelOptionsMenu panelDescriptor={euiContextDescriptors} isViewMode={isViewMode} />;
|
||||
}
|
||||
Default.args = { isViewMode: false } as React.ComponentProps<typeof PanelOptionsMenu>;
|
|
@ -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<PanelOptionsMenuProps> = ({
|
||||
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 = (
|
||||
<EuiButtonIcon
|
||||
iconType={isViewMode ? 'boxesHorizontal' : 'gear'}
|
||||
color="text"
|
||||
className="embPanel__optionsMenuButton"
|
||||
aria-label={title ? enhancedAriaLabel : ariaLabelWithoutTitle}
|
||||
data-test-subj="embeddablePanelToggleMenuIcon"
|
||||
onClick={handleContextMenuClick}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={open}
|
||||
closePopover={handlePopoverClose}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
data-test-subj={open ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'}
|
||||
>
|
||||
<EuiContextMenu initialPanelId="mainMenu" panels={panelDescriptor ? [panelDescriptor] : []} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -112,6 +112,10 @@
|
|||
|
||||
}
|
||||
|
||||
.embPanel__optionsMenuPopover-loading {
|
||||
width: $euiSizeS * 32;
|
||||
}
|
||||
|
||||
.embPanel__optionsMenuPopover-notification::after {
|
||||
position: absolute;
|
||||
top: 0;
|
|
@ -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 (
|
||||
<EuiPanel
|
||||
role="figure"
|
||||
paddingSize="none"
|
||||
hasShadow={showShadow}
|
||||
className={'embPanel embPanel--loading embPanel-isLoading'}
|
||||
data-test-subj="embeddablePanelLoadingIndicator"
|
||||
>
|
||||
<EuiLoadingChart size="l" mono />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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<string, Action>();
|
||||
const triggerRegistry = new Map<string, Trigger>();
|
||||
|
||||
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<ReactWrapper> => {
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mount(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel {...props} />
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={getActions}
|
||||
showNotifications={true}
|
||||
showBadges={true}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
|
||||
findTestSubject(component!, 'embeddablePanelToggleMenuIcon').simulate('click');
|
||||
await nextTick();
|
||||
component!.update();
|
||||
|
||||
return { component: component! };
|
||||
};
|
||||
|
||||
describe('Error states', () => {
|
||||
let component: ReactWrapper<unknown>;
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel embeddable={embeddable} />
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
|
||||
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<typeof embeddable>;
|
||||
let destroyError: jest.MockedFunction<ReturnType<typeof catchError>>;
|
||||
|
||||
(embeddable.catchError as jest.MockedFunction<typeof catchError>).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(<div>Something</div>);
|
||||
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');
|
||||
});
|
|
@ -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<ReactNode | undefined>();
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
|
||||
|
||||
const headerId = useMemo(() => htmlIdGenerator()(), []);
|
||||
const [outputError, setOutputError] = useState<Error>();
|
||||
|
||||
/**
|
||||
* Universal actions are exposed on the context menu for every embeddable, they
|
||||
* bypass the trigger registry.
|
||||
*/
|
||||
const universalActions = useMemo<PanelUniversalActions>(() => {
|
||||
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 (
|
||||
<EuiPanel
|
||||
role="figure"
|
||||
paddingSize="none"
|
||||
className={classes}
|
||||
hasShadow={showShadow}
|
||||
aria-labelledby={headerId}
|
||||
data-test-subj="embeddablePanel"
|
||||
data-test-embeddable-id={embeddable.id}
|
||||
>
|
||||
{!hideHeader && (
|
||||
<EmbeddablePanelHeader
|
||||
{...panelProps}
|
||||
headerId={headerId}
|
||||
universalActions={universalActions}
|
||||
/>
|
||||
)}
|
||||
{outputError && (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="eui-fullHeight embPanel__error"
|
||||
data-test-subj="embeddableError"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EmbeddableErrorHandler embeddable={embeddable} error={outputError}>
|
||||
{(error) => (
|
||||
<EmbeddablePanelError
|
||||
editPanelAction={universalActions.editPanel}
|
||||
embeddable={embeddable}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
</EmbeddableErrorHandler>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
<div className="embPanel__content" ref={embeddableRoot} {...contentAttrs}>
|
||||
{node}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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',
|
||||
});
|
||||
};
|
23
src/plugins/embeddable/public/embeddable_panel/index.tsx
Normal file
23
src/plugins/embeddable/public/embeddable_panel/index.tsx
Normal file
|
@ -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 <EmbeddableLoadingIndicator />;
|
||||
const { embeddable, ...passThroughProps } = props;
|
||||
return <result.Panel embeddable={result.unwrappedEmbeddable} {...passThroughProps} />;
|
||||
};
|
|
@ -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(
|
|
@ -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<TimeRangeInput> {
|
||||
timeRange: TimeRange;
|
|
@ -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 () => {
|
|
@ -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';
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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;
|
|
@ -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<TimeRangeInput>) {
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export * from './customize_panel_action';
|
||||
export * from './custom_time_range_badge';
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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';
|
|
@ -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();
|
|
@ -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';
|
||||
|
|
@ -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());
|
|
@ -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';
|
||||
|
|
@ -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<Array<Action<object>>>([]);
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean | undefined>(undefined);
|
||||
const [contextMenuPanels, setContextMenuPanels] = useState<EuiContextMenuPanelDescriptor[]>([]);
|
||||
|
||||
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<Action<object>>
|
||||
);
|
||||
|
||||
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 = (
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
className="embPanel__optionsMenuButton"
|
||||
data-test-subj="embeddablePanelToggleMenuIcon"
|
||||
aria-label={getContextMenuAriaLabel(title, index)}
|
||||
onClick={() => setIsContextMenuOpen((isOpen) => !isOpen)}
|
||||
iconType={viewMode === ViewMode.VIEW ? 'boxesHorizontal' : 'gear'}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
repositionOnScroll
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
button={ContextMenuButton}
|
||||
isOpen={isContextMenuOpen}
|
||||
className={contextMenuClasses}
|
||||
closePopover={() => setIsContextMenuOpen(false)}
|
||||
data-test-subj={
|
||||
isContextMenuOpen ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
>
|
||||
{menuPanelsLoading ? (
|
||||
<EuiContextMenuPanel
|
||||
className="embPanel__optionsMenuPopover-loading"
|
||||
title={i18n.translate('embeddableApi.panel.contextMenu.loadingTitle', {
|
||||
defaultMessage: 'Options',
|
||||
})}
|
||||
>
|
||||
<EuiContextMenuItem>
|
||||
<EuiSkeletonText />
|
||||
</EuiContextMenuItem>
|
||||
</EuiContextMenuPanel>
|
||||
) : (
|
||||
<EuiContextMenu initialPanelId="mainMenu" panels={contextMenuPanels} />
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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 = (
|
||||
<EuiScreenReaderOnly>
|
||||
<span id={headerId}>{ariaLabel}</span>
|
||||
</EuiScreenReaderOnly>
|
||||
);
|
||||
|
||||
const headerClasses = classNames('embPanel__header', {
|
||||
'embPanel__header--floater': !showPanelBar,
|
||||
});
|
||||
|
||||
const titleClasses = classNames('embPanel__title', {
|
||||
'embPanel--dragHandle': viewMode === ViewMode.EDIT,
|
||||
});
|
||||
|
||||
const embeddablePanelContextMenu = (
|
||||
<EmbeddablePanelContextMenu
|
||||
{...{ index, embeddable, getActions, actionPredicate, universalActions }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!showPanelBar) {
|
||||
return (
|
||||
<div className={headerClasses}>
|
||||
{embeddablePanelContextMenu}
|
||||
{ariaLabelElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<figcaption
|
||||
className={headerClasses}
|
||||
data-test-subj={`embeddablePanelHeading-${(title || '').replace(/\s/g, '')}`}
|
||||
>
|
||||
<h2 data-test-subj="dashboardPanelTitle" className={titleClasses}>
|
||||
{ariaLabelElement}
|
||||
<EmbeddablePanelTitle
|
||||
viewMode={viewMode}
|
||||
hideTitle={hideTitle}
|
||||
embeddable={embeddable}
|
||||
description={description}
|
||||
customizePanelAction={universalActions.customizePanel}
|
||||
/>
|
||||
{showBadges && badgeComponents}
|
||||
</h2>
|
||||
{showNotifications && notificationComponents}
|
||||
{embeddablePanelContextMenu}
|
||||
</figcaption>
|
||||
);
|
||||
};
|
|
@ -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 <span className={titleClassNames}>{title}</span>;
|
||||
}
|
||||
if (customizePanelAction) {
|
||||
return (
|
||||
<EuiLink
|
||||
color="text"
|
||||
className={titleClassNames}
|
||||
aria-label={getEditTitleAriaLabel(title)}
|
||||
data-test-subj={'embeddablePanelTitleLink'}
|
||||
onClick={() => customizePanelAction.execute({ embeddable })}
|
||||
>
|
||||
{title || placeholderTitle}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [customizePanelAction, embeddable, title, viewMode, hideTitle]);
|
||||
|
||||
const titleComponentWithDescription = useMemo(() => {
|
||||
if (!description) return <span className="embPanel__titleInner">{titleComponent}</span>;
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={description}
|
||||
delay="regular"
|
||||
position="top"
|
||||
anchorClassName="embPanel__titleTooltipAnchor"
|
||||
>
|
||||
<span className="embPanel__titleInner">
|
||||
{titleComponent} <EuiIcon type="iInCircle" color="subdued" />
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}, [description, titleComponent]);
|
||||
|
||||
return titleComponentWithDescription;
|
||||
};
|
|
@ -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<EmbeddableBadgeAction[]>();
|
||||
const [notifications, setNotifications] = useState<EmbeddableNotificationAction[]>();
|
||||
|
||||
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) => (
|
||||
<EuiBadge
|
||||
key={badge.id}
|
||||
className="embPanel__headerBadge"
|
||||
iconType={badge.getIconType({ embeddable, trigger: panelBadgeTrigger })}
|
||||
onClick={() => badge.execute({ embeddable, trigger: panelBadgeTrigger })}
|
||||
onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
|
||||
data-test-subj={`embeddablePanelBadge-${badge.id}`}
|
||||
>
|
||||
{badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
|
||||
</EuiBadge>
|
||||
)),
|
||||
[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,
|
||||
},
|
||||
})
|
||||
) : (
|
||||
<EuiNotificationBadge
|
||||
data-test-subj={`embeddablePanelNotification-${notification.id}`}
|
||||
key={notification.id}
|
||||
style={{ marginTop: euiThemeVars.euiSizeXS, marginRight: euiThemeVars.euiSizeXS }}
|
||||
onClick={() => notification.execute({ ...context, trigger: panelNotificationTrigger })}
|
||||
>
|
||||
{notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })}
|
||||
</EuiNotificationBadge>
|
||||
);
|
||||
|
||||
if (notification.getDisplayNameTooltip) {
|
||||
const tooltip = notification.getDisplayNameTooltip({
|
||||
...context,
|
||||
trigger: panelNotificationTrigger,
|
||||
});
|
||||
|
||||
if (tooltip) {
|
||||
badge = (
|
||||
<EuiToolTip position="top" delay="regular" content={tooltip} key={notification.id}>
|
||||
{badge}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return badge;
|
||||
}),
|
||||
[embeddable, notifications]
|
||||
);
|
||||
|
||||
return { badgeComponents, notificationComponents };
|
||||
};
|
86
src/plugins/embeddable/public/embeddable_panel/types.ts
Normal file
86
src/plugins/embeddable/public/embeddable_panel/types.ts
Normal file
|
@ -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<IEmbeddable<EmbeddableInput, EmbeddableOutput>>
|
||||
>;
|
||||
|
||||
export type EmbeddableNotificationAction = Action<
|
||||
EmbeddableContext<IEmbeddable<EmbeddableInput, EmbeddableOutput>>
|
||||
>;
|
||||
|
||||
type PanelEmbeddable = IEmbeddable<EmbeddableInput, EmbeddableOutput, MaybePromise<ReactNode>>;
|
||||
|
||||
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<PanelEmbeddable>);
|
||||
|
||||
/**
|
||||
* 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<EmbeddablePanelProps, 'embeddable'> & {
|
||||
embeddable: PanelEmbeddable;
|
||||
};
|
||||
|
||||
export interface InspectorPanelAction {
|
||||
inspectPanel: InspectPanelAction;
|
||||
}
|
||||
|
||||
export interface BasePanelActions {
|
||||
customizePanel: CustomizePanelAction;
|
||||
inspectPanel: InspectPanelAction;
|
||||
removePanel: RemovePanelAction;
|
||||
editPanel: EditPanelAction;
|
||||
}
|
||||
|
||||
export interface PanelUniversalActions
|
||||
extends Partial<InspectorPanelAction>,
|
||||
Partial<BasePanelActions> {}
|
|
@ -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<Promise<IEmbeddable | ErrorEmbeddable>, []>()
|
||||
.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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
): InputType[KeyType] | undefined => {
|
||||
const [value, setValue] = useState<InputType[KeyType] | undefined>(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<EmbeddableInput, OutputType>
|
||||
): OutputType[KeyType] => {
|
||||
const [value, setValue] = useState<OutputType[KeyType]>(embeddable.getOutput()[key]);
|
||||
useEffect(() => {
|
||||
const subscription = embeddable
|
||||
.getOutput$()
|
||||
.pipe(distinctUntilKeyChanged(key))
|
||||
.subscribe(() => setValue(embeddable.getOutput()[key]));
|
||||
return () => subscription.unsubscribe();
|
||||
}, [embeddable, key]);
|
||||
|
||||
return value;
|
||||
};
|
|
@ -1,3 +1,2 @@
|
|||
@import './variables';
|
||||
@import './lib/panel/index';
|
||||
@import './lib/panel/panel_header/index';
|
||||
@import './embeddable_panel/embeddable_panel';
|
||||
|
|
|
@ -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';
|
||||
|
|
50
src/plugins/embeddable/public/kibana_services.ts
Normal file
50
src/plugins/embeddable/public/kibana_services.ts
Normal file
|
@ -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<void>((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);
|
||||
};
|
|
@ -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';
|
|
@ -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(
|
||||
<EmbeddableChildPanel
|
||||
container={container}
|
||||
embeddableId={newEmbeddable.id}
|
||||
PanelComponent={testPanel}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<EmbeddableChildPanel container={container} embeddableId={'1'} PanelComponent={testPanel} />
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
component.update();
|
||||
|
||||
expect(
|
||||
component.getDOMNode().querySelectorAll('[data-test-subj="embeddableStackError"]').length
|
||||
).toBe(1);
|
||||
});
|
|
@ -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<EmbeddableChildPanelProps, State> {
|
||||
[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 (
|
||||
<div className={classes}>
|
||||
{this.state.firstTimeLoading || !this.embeddable ? (
|
||||
<EuiLoadingChart size="l" mono />
|
||||
) : (
|
||||
<PanelComponent embeddable={this.embeddable} index={index} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,4 +14,3 @@ export type {
|
|||
EmbeddableContainerSettings,
|
||||
} from './i_container';
|
||||
export { Container } from './container';
|
||||
export * from './embeddable_child_panel';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import './embeddable_panel';
|
|
@ -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<string, Action>();
|
||||
const triggerRegistry = new Map<string, Trigger>();
|
||||
|
||||
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<ContactCardEmbeddable>('123');
|
||||
expect(embeddable).toBeDefined();
|
||||
expect(embeddable.id).toBe('123');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
if (container.getOutput().embeddableLoaded['123']) {
|
||||
const embeddable = container.getChild<ContactCardEmbeddable>('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<ContactCardEmbeddableInput>(
|
||||
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<ContactCardEmbeddable>(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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
application={applicationMock}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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<unknown>;
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
application={applicationMock}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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<typeof embeddable>;
|
||||
let destroyError: jest.MockedFunction<ReturnType<typeof catchError>>;
|
||||
|
||||
(embeddable.catchError as jest.MockedFunction<typeof catchError>).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(<div>Something</div>);
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={getActions}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => 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(
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => 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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={selfStyledEmbeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
hideHeader={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => 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}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
inspector={inspector}
|
||||
hideHeader={false}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<I18nProvider>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
SavedObjectFinder={() => null}
|
||||
theme={theme}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
expect(component.find('.embPanel__titleText').text()).toBe('Hello Bran Stark');
|
||||
});
|
|
@ -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<EmbeddableInput, EmbeddableOutput, MaybePromise<ReactNode>>;
|
||||
|
||||
/**
|
||||
* 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<any>;
|
||||
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<Action<EmbeddableContext>>;
|
||||
notifications: Array<Action<EmbeddableContext>>;
|
||||
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<InspectorPanelAction>, Partial<BasePanelActions> {}
|
||||
|
||||
export class EmbeddablePanel extends React.Component<Props, State> {
|
||||
private embeddableRoot = React.createRef<HTMLDivElement>();
|
||||
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<IEmbeddable<EmbeddableInput, EmbeddableOutput, any>>
|
||||
>;
|
||||
|
||||
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<IEmbeddable<EmbeddableInput, EmbeddableOutput, any>>
|
||||
>;
|
||||
|
||||
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 (
|
||||
<EuiPanel
|
||||
className={classes}
|
||||
data-test-subj="embeddablePanel"
|
||||
data-test-embeddable-id={this.props.embeddable.id}
|
||||
paddingSize="none"
|
||||
role="figure"
|
||||
aria-labelledby={headerId}
|
||||
hasShadow={this.props.showShadow}
|
||||
>
|
||||
{!this.props.hideHeader && (
|
||||
<PanelHeader
|
||||
getActionContextMenuPanel={this.getActionContextMenuPanel}
|
||||
hidePanelTitle={this.state.hidePanelTitle || !!selfStyledOptions?.hideTitle}
|
||||
isViewMode={viewOnlyMode}
|
||||
customizePanel={
|
||||
'customizePanel' in this.state.universalActions
|
||||
? this.state.universalActions.customizePanel
|
||||
: undefined
|
||||
}
|
||||
closeContextMenu={this.state.closeContextMenu}
|
||||
title={title}
|
||||
description={description}
|
||||
index={this.props.index}
|
||||
badges={this.state.badges}
|
||||
notifications={this.state.notifications}
|
||||
embeddable={this.props.embeddable}
|
||||
headerId={headerId}
|
||||
/>
|
||||
)}
|
||||
{this.state.error && (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="eui-fullHeight embPanel__error"
|
||||
data-test-subj="embeddableError"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EmbeddableErrorHandler embeddable={this.props.embeddable} error={this.state.error}>
|
||||
{(error) => (
|
||||
<EmbeddablePanelError
|
||||
editPanelAction={this.state.universalActions.editPanel}
|
||||
embeddable={this.props.embeddable}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
</EmbeddableErrorHandler>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
<div className="embPanel__content" ref={this.embeddableRoot} {...contentAttrs}>
|
||||
{this.state.node}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
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<Action<object>>)
|
||||
.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,
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './panel_options_menu_form';
|
|
@ -1,3 +0,0 @@
|
|||
.embPanel__optionsMenuForm {
|
||||
padding: $euiSize;
|
||||
}
|
|
@ -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';
|
|
@ -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>(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();
|
||||
});
|
|
@ -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<ActionContext> {
|
||||
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<any>,
|
||||
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<ActionContext>) {
|
||||
const { embeddable } = context;
|
||||
return embeddable.getIsContainer() && embeddable.getInput().viewMode === ViewMode.EDIT;
|
||||
}
|
||||
|
||||
public async execute(context: ActionExecutionContext<ActionContext>) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div>
|
||||
<div>Hello World</div>
|
||||
{props.children}
|
||||
</div>
|
||||
) 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(
|
||||
<AddPanelFlyout
|
||||
container={container}
|
||||
onClose={onClose}
|
||||
getFactory={getEmbeddableFactory}
|
||||
getAllFactories={start.getEmbeddableFactories}
|
||||
notifications={core.notifications}
|
||||
SavedObjectFinder={() => null}
|
||||
showCreateNewMenu
|
||||
/>
|
||||
) as ReactWrapper<unknown, unknown, AddPanelFlyout>;
|
||||
|
||||
// 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<ContactCardEmbeddable>(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(
|
||||
<AddPanelFlyout
|
||||
container={container}
|
||||
onClose={onClose}
|
||||
getFactory={getEmbeddableFactory}
|
||||
getAllFactories={start.getEmbeddableFactories}
|
||||
notifications={core.notifications}
|
||||
SavedObjectFinder={(props) => <DummySavedObjectFinder {...props} />}
|
||||
showCreateNewMenu
|
||||
/>
|
||||
) as ReactWrapper<any, {}, AddPanelFlyout>;
|
||||
|
||||
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);
|
||||
});
|
|
@ -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<any>;
|
||||
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<Props, State> {
|
||||
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<SavedObjectAttributes>
|
||||
) => {
|
||||
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<SavedObjectEmbeddableInput>(
|
||||
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<SavedObjectAttributes>
|
||||
) {
|
||||
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) => (
|
||||
<EuiContextMenuItem
|
||||
key={factory.type}
|
||||
data-test-subj={`createNew-${factory.type}`}
|
||||
onClick={() => this.createNewEmbeddable(factory.type)}
|
||||
className="embPanel__addItem"
|
||||
>
|
||||
{capitalize(factory.getDisplayName())}
|
||||
</EuiContextMenuItem>
|
||||
));
|
||||
}
|
||||
|
||||
public render() {
|
||||
const SavedObjectFinder = this.props.SavedObjectFinder;
|
||||
const metaData = [...this.props.getAllFactories()]
|
||||
.filter(
|
||||
(embeddableFactory) =>
|
||||
Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
|
||||
)
|
||||
.map(({ savedObjectMetaData }) => savedObjectMetaData);
|
||||
const savedObjectsFinder = (
|
||||
<SavedObjectFinder
|
||||
onChoose={this.onAddPanel}
|
||||
savedObjectMetaData={metaData}
|
||||
showFilter={true}
|
||||
noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', {
|
||||
defaultMessage: 'No matching objects found.',
|
||||
})}
|
||||
>
|
||||
{this.props.showCreateNewMenu ? (
|
||||
<SavedObjectFinderCreateNew menuItems={this.getCreateMenuItems()} />
|
||||
) : null}
|
||||
</SavedObjectFinder>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="embeddableApi.addPanel.Title"
|
||||
defaultMessage="Add from library"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{savedObjectsFinder}</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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<any>;
|
||||
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(
|
||||
<AddPanelFlyout
|
||||
container={embeddable}
|
||||
onAddPanel={onAddPanel}
|
||||
onClose={() => {
|
||||
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;
|
||||
}
|
|
@ -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 (
|
||||
<EuiPopover
|
||||
id="createNew"
|
||||
button={
|
||||
<EuiButton
|
||||
data-test-subj="createNew"
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
onClick={toggleCreateMenu}
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="embeddableApi.addPanel.createNewDefaultOption"
|
||||
defaultMessage="Create new"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={isCreateMenuOpen}
|
||||
closePopover={closeCreateMenu}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiContextMenuPanel items={menuItems} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -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(<SavedObjectFinderCreateNew menuItems={[]} />);
|
||||
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(
|
||||
<EuiContextMenuItem key={i + 1} data-test-subj={`item${i + 1}`} onClick={onClick}>{`item${
|
||||
i + 1
|
||||
}`}</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const wrapper = shallow(<SavedObjectFinderCreateNew menuItems={items} />);
|
||||
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(
|
||||
<EuiContextMenuItem key={i + 1} data-test-subj={`item${i + 1}`} onClick={onClick}>{`item${
|
||||
i + 1
|
||||
}`}</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const component = mountWithIntl(<SavedObjectFinderCreateNew menuItems={items} />);
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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<Action<EmbeddableContext>>;
|
||||
notifications: Array<Action<EmbeddableContext>>;
|
||||
embeddable: IEmbeddable;
|
||||
headerId?: string;
|
||||
showPlaceholderTitle?: boolean;
|
||||
customizePanel?: CustomizePanelAction;
|
||||
}
|
||||
|
||||
function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmbeddable) {
|
||||
return badges.map((badge) => (
|
||||
<EuiBadge
|
||||
key={badge.id}
|
||||
className="embPanel__headerBadge"
|
||||
iconType={badge.getIconType({ embeddable, trigger: panelBadgeTrigger })}
|
||||
onClick={() => badge.execute({ embeddable, trigger: panelBadgeTrigger })}
|
||||
onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
|
||||
data-test-subj={`embeddablePanelBadge-${badge.id}`}
|
||||
>
|
||||
{badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
|
||||
</EuiBadge>
|
||||
));
|
||||
}
|
||||
|
||||
function renderNotifications(
|
||||
notifications: Array<Action<EmbeddableContext>>,
|
||||
embeddable: IEmbeddable
|
||||
) {
|
||||
return notifications.map((notification) => {
|
||||
const context = { embeddable };
|
||||
|
||||
let badge = notification.MenuItem ? (
|
||||
React.createElement(notification.MenuItem, {
|
||||
key: notification.id,
|
||||
context: {
|
||||
embeddable,
|
||||
trigger: panelNotificationTrigger,
|
||||
},
|
||||
})
|
||||
) : (
|
||||
<EuiNotificationBadge
|
||||
data-test-subj={`embeddablePanelNotification-${notification.id}`}
|
||||
key={notification.id}
|
||||
style={{ marginTop: '4px', marginRight: '4px' }}
|
||||
onClick={() => notification.execute({ ...context, trigger: panelNotificationTrigger })}
|
||||
>
|
||||
{notification.getDisplayName({ ...context, trigger: panelNotificationTrigger })}
|
||||
</EuiNotificationBadge>
|
||||
);
|
||||
|
||||
if (notification.getDisplayNameTooltip) {
|
||||
const tooltip = notification.getDisplayNameTooltip({
|
||||
...context,
|
||||
trigger: panelNotificationTrigger,
|
||||
});
|
||||
|
||||
if (tooltip) {
|
||||
badge = (
|
||||
<EuiToolTip position="top" delay="regular" content={tooltip} key={notification.id}>
|
||||
{badge}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<span id={headerId}>
|
||||
{showPanelBar && title
|
||||
? i18n.translate('embeddableApi.panel.enhancedDashboardPanelAriaLabel', {
|
||||
defaultMessage: 'Dashboard panel: {title}',
|
||||
values: { title: title || placeholderTitle },
|
||||
})
|
||||
: i18n.translate('embeddableApi.panel.dashboardPanelAriaLabel', {
|
||||
defaultMessage: 'Dashboard panel',
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (!showPanelBar) {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<PanelOptionsMenu
|
||||
getActionContextMenuPanel={getActionContextMenuPanel}
|
||||
isViewMode={isViewMode}
|
||||
closeContextMenu={closeContextMenu}
|
||||
title={title}
|
||||
index={index}
|
||||
/>
|
||||
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderTitle = () => {
|
||||
let titleComponent;
|
||||
if (showTitle) {
|
||||
titleComponent = isViewMode ? (
|
||||
<span
|
||||
className={classNames('embPanel__titleText', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
embPanel__placeholderTitleText: !title,
|
||||
})}
|
||||
>
|
||||
{title || placeholderTitle}
|
||||
</span>
|
||||
) : customizePanel ? (
|
||||
<EuiLink
|
||||
color="text"
|
||||
data-test-subj={'embeddablePanelTitleLink'}
|
||||
className={classNames('embPanel__titleText', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
embPanel__placeholderTitleText: !title,
|
||||
})}
|
||||
aria-label={i18n.translate('embeddableApi.panel.editTitleAriaLabel', {
|
||||
defaultMessage: 'Click to edit title: {title}',
|
||||
values: { title: title || placeholderTitle },
|
||||
})}
|
||||
onClick={() => customizePanel.execute({ embeddable })}
|
||||
>
|
||||
{title || placeholderTitle}
|
||||
</EuiLink>
|
||||
) : null;
|
||||
}
|
||||
return description ? (
|
||||
<EuiToolTip
|
||||
content={description}
|
||||
delay="regular"
|
||||
position="top"
|
||||
anchorClassName="embPanel__titleTooltipAnchor"
|
||||
>
|
||||
<span className="embPanel__titleInner">
|
||||
{titleComponent} <EuiIcon type="iInCircle" color="subdued" />
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<span className="embPanel__titleInner">{titleComponent}</span>
|
||||
);
|
||||
};
|
||||
|
||||
const titleClasses = classNames('embPanel__title', { 'embPanel--dragHandle': !isViewMode });
|
||||
|
||||
return (
|
||||
<figcaption
|
||||
className={classes}
|
||||
data-test-subj={`embeddablePanelHeading-${(title || '').replace(/\s/g, '')}`}
|
||||
>
|
||||
<h2 data-test-subj="dashboardPanelTitle" className={titleClasses}>
|
||||
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly>
|
||||
{renderTitle()}
|
||||
{renderBadges(badges, embeddable)}
|
||||
</h2>
|
||||
{renderNotifications(notifications, embeddable)}
|
||||
<PanelOptionsMenu
|
||||
isViewMode={isViewMode}
|
||||
getActionContextMenuPanel={getActionContextMenuPanel}
|
||||
closeContextMenu={closeContextMenu}
|
||||
title={title}
|
||||
index={index}
|
||||
/>
|
||||
</figcaption>
|
||||
);
|
||||
}
|
|
@ -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<PanelOptionsMenuProps, State> {
|
||||
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 = (
|
||||
<EuiButtonIcon
|
||||
iconType={isViewMode ? 'boxesHorizontal' : 'gear'}
|
||||
color="text"
|
||||
className="embPanel__optionsMenuButton"
|
||||
aria-label={title ? enhancedAriaLabel : ariaLabelWithoutTitle}
|
||||
data-test-subj="embeddablePanelToggleMenuIcon"
|
||||
onClick={this.toggleContextMenu}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
className={
|
||||
'embPanel__optionsMenuPopover' +
|
||||
(this.state.showNotification ? ' embPanel__optionsMenuPopover-notification' : '')
|
||||
}
|
||||
button={button}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
data-test-subj={
|
||||
this.state.isPopoverOpen
|
||||
? 'embeddablePanelContextMenuOpen'
|
||||
: 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId="mainMenu"
|
||||
panels={this.state.actionContextMenuPanel?.panels || []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
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);
|
||||
};
|
||||
}
|
|
@ -23,6 +23,11 @@ export class ContactCardEmbeddableFactory
|
|||
implements EmbeddableFactoryDefinition<ContactCardEmbeddableInput>
|
||||
{
|
||||
public readonly type = CONTACT_CARD_EMBEDDABLE;
|
||||
savedObjectMetaData = {
|
||||
name: 'Contact card',
|
||||
type: CONTACT_CARD_EMBEDDABLE,
|
||||
getIconForSavedObject: () => 'document',
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected readonly execTrigger: UiActionsStart['executeTriggerActions'],
|
||||
|
|
|
@ -33,7 +33,6 @@ interface HelloWorldContainerInput extends ContainerInput {
|
|||
|
||||
interface HelloWorldContainerOptions {
|
||||
getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'];
|
||||
panelComponent?: EmbeddableStart['EmbeddablePanel'];
|
||||
}
|
||||
|
||||
export class HelloWorldContainer extends Container<InheritedInput, HelloWorldContainerInput> {
|
||||
|
@ -41,7 +40,7 @@ export class HelloWorldContainer extends Container<InheritedInput, HelloWorldCon
|
|||
|
||||
constructor(
|
||||
input: ContainerInput<{ firstName: string; lastName: string }>,
|
||||
private readonly options: HelloWorldContainerOptions,
|
||||
options: HelloWorldContainerOptions,
|
||||
initializeSettings?: EmbeddableContainerSettings
|
||||
) {
|
||||
super(
|
||||
|
@ -64,14 +63,7 @@ export class HelloWorldContainer extends Container<InheritedInput, HelloWorldCon
|
|||
public render(node: HTMLElement) {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
{this.options.panelComponent ? (
|
||||
<HelloWorldContainerComponent
|
||||
container={this}
|
||||
panelComponent={this.options.panelComponent}
|
||||
/>
|
||||
) : (
|
||||
<div>Panel component not provided.</div>
|
||||
)}
|
||||
<HelloWorldContainerComponent container={this} />
|
||||
</I18nProvider>,
|
||||
node
|
||||
);
|
||||
|
|
|
@ -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<Props, State> {
|
|||
const list = Object.values(this.state.panels).map((panelState) => {
|
||||
const item = (
|
||||
<EuiFlexItem key={panelState.explicitInput.id}>
|
||||
<EmbeddableChildPanel
|
||||
container={this.props.container}
|
||||
embeddableId={panelState.explicitInput.id}
|
||||
PanelComponent={this.props.panelComponent}
|
||||
<EmbeddablePanel
|
||||
embeddable={() =>
|
||||
this.props.container.untilEmbeddableLoaded(panelState.explicitInput.id)
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -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<EmbeddableSetup>;
|
||||
export type Start = jest.Mocked<EmbeddableStart>;
|
||||
|
||||
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<any>;
|
||||
}
|
||||
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
|
||||
export const createEmbeddablePanelMock = ({
|
||||
getActions,
|
||||
getEmbeddableFactory,
|
||||
getAllEmbeddableFactories,
|
||||
overlays,
|
||||
notifications,
|
||||
application,
|
||||
inspector,
|
||||
SavedObjectFinder,
|
||||
}: Partial<CreateEmbeddablePanelMockArgs>) => {
|
||||
return ({ embeddable }: { embeddable: IEmbeddable }) => (
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={getActions || (() => 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<EmbeddableStateTransfer> => {
|
||||
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<EmbeddableSetupDependencies> = {})
|
|||
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() },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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<EmbeddableState
|
|||
embeddableFactoryId: string
|
||||
) => EmbeddableFactory<I, O, E> | undefined;
|
||||
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
|
||||
EmbeddablePanel: EmbeddablePanelHOC;
|
||||
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
|
||||
getAttributeService: <
|
||||
A extends { title: string },
|
||||
|
@ -106,14 +104,6 @@ export interface EmbeddableStart extends PersistableStateService<EmbeddableState
|
|||
options: AttributeServiceOptions<A, M>
|
||||
) => AttributeService<A, V, R, M>;
|
||||
}
|
||||
|
||||
export type EmbeddablePanelHOC = React.FC<{
|
||||
embeddable: IEmbeddable;
|
||||
hideHeader?: boolean;
|
||||
containerContext?: EmbeddableContainerContext;
|
||||
index?: number;
|
||||
}>;
|
||||
|
||||
export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, EmbeddableStart> {
|
||||
private readonly embeddableFactoryDefinitions: Map<string, EmbeddableFactoryDefinition> =
|
||||
new Map();
|
||||
|
@ -145,15 +135,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
};
|
||||
}
|
||||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{
|
||||
uiActions,
|
||||
inspector,
|
||||
savedObjectsManagement,
|
||||
savedObjectsTaggingOss,
|
||||
}: EmbeddableStartDependencies
|
||||
): EmbeddableStart {
|
||||
public start(core: CoreStart, deps: EmbeddableStartDependencies): EmbeddableStart {
|
||||
this.embeddableFactoryDefinitions.forEach((def) => {
|
||||
this.embeddableFactories.set(
|
||||
def.type,
|
||||
|
@ -163,6 +145,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
);
|
||||
});
|
||||
|
||||
const { uiActions } = deps;
|
||||
const { overlays, theme, uiSettings } = core;
|
||||
|
||||
const dateFormat = uiSettings.get(UI_SETTINGS.DATE_FORMAT);
|
||||
|
@ -188,45 +171,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
);
|
||||
this.isRegistryReady = true;
|
||||
|
||||
const getEmbeddablePanelHoc =
|
||||
() =>
|
||||
({
|
||||
embeddable,
|
||||
hideHeader,
|
||||
containerContext,
|
||||
index,
|
||||
}: {
|
||||
embeddable: IEmbeddable;
|
||||
hideHeader?: boolean;
|
||||
containerContext?: EmbeddableContainerContext;
|
||||
index?: number;
|
||||
}) =>
|
||||
(
|
||||
<EmbeddablePanel
|
||||
hideHeader={hideHeader}
|
||||
embeddable={embeddable}
|
||||
index={index}
|
||||
stateTransfer={this.stateTransferService}
|
||||
getActions={uiActions.getTriggerCompatibleActions}
|
||||
getEmbeddableFactory={this.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={this.getEmbeddableFactories}
|
||||
dateFormat={dateFormat}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
overlays={overlays}
|
||||
notifications={core.notifications}
|
||||
application={core.application}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={getSavedObjectFinder(
|
||||
core.uiSettings,
|
||||
core.http,
|
||||
savedObjectsManagement,
|
||||
savedObjectsTaggingOss?.getTaggingApi()
|
||||
)}
|
||||
containerContext={containerContext}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const commonContract: CommonEmbeddableStartContract = {
|
||||
getEmbeddableFactory: this
|
||||
.getEmbeddableFactory as unknown as CommonEmbeddableStartContract['getEmbeddableFactory'],
|
||||
|
@ -240,7 +184,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
getMigrateFunction(commonContract)
|
||||
);
|
||||
|
||||
return {
|
||||
const embeddableStart: EmbeddableStart = {
|
||||
getEmbeddableFactory: this.getEmbeddableFactory,
|
||||
getEmbeddableFactories: this.getEmbeddableFactories,
|
||||
getAttributeService: (type: string, options) =>
|
||||
|
@ -254,7 +198,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
storage
|
||||
)
|
||||
: this.stateTransferService,
|
||||
EmbeddablePanel: getEmbeddablePanelHoc(),
|
||||
telemetry: getTelemetryFunction(commonContract),
|
||||
extract: getExtractFunction(commonContract),
|
||||
inject: getInjectFunction(commonContract),
|
||||
|
@ -263,6 +206,9 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
return migrateToLatest(getAllMigrationsFn(), state) as EmbeddableStateWithType;
|
||||
},
|
||||
};
|
||||
|
||||
setKibanaServices(core, embeddableStart, deps);
|
||||
return embeddableStart;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
|
|
@ -39,7 +39,6 @@ import {
|
|||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { testPlugin } from './test_plugin';
|
||||
import { of } from './helpers';
|
||||
import { createEmbeddablePanelMock } from '../mocks';
|
||||
import { EmbeddableContainerSettings } from '../lib/containers/i_container';
|
||||
|
||||
async function createHelloWorldContainerAndEmbeddable(
|
||||
|
@ -64,20 +63,10 @@ async function createHelloWorldContainerAndEmbeddable(
|
|||
|
||||
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(
|
||||
containerInput,
|
||||
{
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
panelComponent: testPanel,
|
||||
},
|
||||
settings
|
||||
);
|
||||
|
@ -97,7 +86,6 @@ async function createHelloWorldContainerAndEmbeddable(
|
|||
start,
|
||||
coreSetup,
|
||||
coreStart,
|
||||
testPanel,
|
||||
container,
|
||||
uiActions,
|
||||
embeddable,
|
||||
|
@ -240,7 +228,7 @@ test('Container.addNewEmbeddable', async () => {
|
|||
});
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@ export const testPlugin = (
|
|||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
savedObjectsManagement:
|
||||
savedObjectsManagementMock as unknown as SavedObjectsManagementPluginStart,
|
||||
usageCollection: { reportUiCounter: jest.fn() },
|
||||
});
|
||||
return start;
|
||||
},
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 (
|
||||
<plugins.embeddable.EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
containerContext={embeddableContainerContext}
|
||||
/>
|
||||
<EmbeddablePanel embeddable={embeddable} containerContext={embeddableContainerContext} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
|||
<EmbeddablePanelWrapper
|
||||
factory={factory}
|
||||
uiActions={uiActions}
|
||||
inspector={inspector}
|
||||
actionPredicate={() => hasActions}
|
||||
input={input}
|
||||
theme={theme}
|
||||
extraActions={input.extraActions}
|
||||
showInspector={input.showInspector}
|
||||
withDefaultActions={input.withDefaultActions}
|
||||
|
@ -137,10 +134,8 @@ function EmbeddableRootWrapper({
|
|||
interface EmbeddablePanelWrapperProps {
|
||||
factory: EmbeddableFactory<EmbeddableInput, EmbeddableOutput>;
|
||||
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<EmbeddablePanelWrapperProps> = ({
|
|||
factory,
|
||||
uiActions,
|
||||
actionPredicate,
|
||||
inspector,
|
||||
input,
|
||||
theme,
|
||||
extraActions,
|
||||
showInspector = true,
|
||||
withDefaultActions,
|
||||
|
@ -179,12 +172,11 @@ const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({
|
|||
|
||||
return [...(extraActions ?? []), ...actions];
|
||||
}}
|
||||
inspector={showInspector ? inspector : undefined}
|
||||
hideInspector={!showInspector}
|
||||
actionPredicate={actionPredicate}
|
||||
showNotifications={false}
|
||||
showShadow={false}
|
||||
showBadges={false}
|
||||
showNotifications={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(() => <div data-test-subj="EmbeddablePanel" />),
|
||||
},
|
||||
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(<div data-test-subj="EmbeddablePanel" />),
|
||||
}));
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<EmbeddedMapComponent {...testProps} />
|
||||
|
|
|
@ -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 ? (
|
||||
<IndexPatternsMissingPrompt data-test-subj="missing-prompt" />
|
||||
) : embeddable != null ? (
|
||||
<services.embeddable.EmbeddablePanel embeddable={embeddable} />
|
||||
<EmbeddablePanel embeddable={embeddable} />
|
||||
) : (
|
||||
<Loader data-test-subj="loading-panel" overlay size="xl" />
|
||||
)}
|
||||
</EmbeddableMap>
|
||||
</Embeddable>
|
||||
);
|
||||
}, [embeddable, isIndexError, portalNode, services, storageValue]);
|
||||
}, [embeddable, isIndexError, portalNode, storageValue]);
|
||||
|
||||
return isError ? null : (
|
||||
<StyledEuiAccordion
|
||||
|
|
|
@ -2520,7 +2520,6 @@
|
|||
"domDragDrop.dropTargets.swap": "Permuter",
|
||||
"domDragDrop.keyboardInstructions": "Appuyez sur la barre d'espace ou sur Entrée pour commencer à faire glisser. Lors du glissement, utilisez les touches fléchées gauche/droite pour vous déplacer entre les cibles de dépôt. Appuyez à nouveau sur la barre d'espace ou sur Entrée pour terminer.",
|
||||
"domDragDrop.keyboardInstructionsReorder": "Appuyez sur la barre d'espace ou sur Entrée pour commencer à faire glisser. Lors du glissement, utilisez les touches fléchées haut/bas pour réorganiser les éléments dans le groupe et les touches gauche/droite pour choisir les cibles de dépôt à l'extérieur du groupe. Appuyez à nouveau sur la barre d'espace ou sur Entrée pour terminer.",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "Une erreur s'est produite lors de l'enregistrement. Erreur : {errorMessage}",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.",
|
||||
"embeddableApi.panel.editPanel.displayName": "Modifier {value}",
|
||||
|
@ -2528,10 +2527,6 @@
|
|||
"embeddableApi.panel.enhancedDashboardPanelAriaLabel": "Panneau du tableau de bord : {title}",
|
||||
"embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabelWithIndex": "Options pour le panneau {index}",
|
||||
"embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel": "Options de panneau pour {title}",
|
||||
"embeddableApi.addPanel.createNewDefaultOption": "Créer",
|
||||
"embeddableApi.addPanel.displayName": "Ajouter un panneau",
|
||||
"embeddableApi.addPanel.noMatchingObjectsMessage": "Aucun objet correspondant trouvé.",
|
||||
"embeddableApi.addPanel.Title": "Ajouter depuis la bibliothèque",
|
||||
"embeddableApi.cellValueTrigger.description": "Les actions apparaissent dans les options de valeur de cellule dans la visualisation",
|
||||
"embeddableApi.cellValueTrigger.title": "Valeur de cellule",
|
||||
"embeddableApi.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau",
|
||||
|
|
|
@ -2520,7 +2520,6 @@
|
|||
"domDragDrop.dropTargets.swap": "入れ替える",
|
||||
"domDragDrop.keyboardInstructions": "スペースまたはEnterを押してドラッグを開始します。ドラッグするときには、左右の矢印キーを使用して、ドロップ対象間を移動します。もう一度スペースまたはEnterを押すと終了します。",
|
||||
"domDragDrop.keyboardInstructionsReorder": "スペースまたはEnterを押してドラッグを開始します。ドラッグするときには、上下矢印キーを使用すると、グループの項目を並べ替えます。左右矢印キーを使用すると、グループの外側でドロップ対象を選択します。もう一度スペースまたはEnterを押すと終了します。",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName}が追加されました",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "保存中にエラーが発生しました。エラー:{errorMessage}",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "{type}を読み込めません。Elasticsearch と Kibanaのデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。",
|
||||
"embeddableApi.panel.editPanel.displayName": "{value}の編集",
|
||||
|
@ -2528,10 +2527,6 @@
|
|||
"embeddableApi.panel.enhancedDashboardPanelAriaLabel": "ダッシュボードパネル:{title}",
|
||||
"embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabelWithIndex": "パネル{index}のオプション",
|
||||
"embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel": "{title}のパネルオプション",
|
||||
"embeddableApi.addPanel.createNewDefaultOption": "新規作成",
|
||||
"embeddableApi.addPanel.displayName": "パネルの追加",
|
||||
"embeddableApi.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。",
|
||||
"embeddableApi.addPanel.Title": "ライブラリから追加",
|
||||
"embeddableApi.cellValueTrigger.description": "アクションはビジュアライゼーションのセル値オプションに表示されます",
|
||||
"embeddableApi.cellValueTrigger.title": "セル値",
|
||||
"embeddableApi.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます",
|
||||
|
|
|
@ -2519,7 +2519,6 @@
|
|||
"domDragDrop.dropTargets.swap": "交换",
|
||||
"domDragDrop.keyboardInstructions": "按空格键或 enter 键开始拖动。拖动时,请左右箭头键在拖动目标之间移动。再次按空格键或 enter 键结束操作。",
|
||||
"domDragDrop.keyboardInstructionsReorder": "按空格键或 enter 键开始拖动。拖动时,请使用上下箭头键重新排列组中的项目,使用左右箭头键在组之外选择拖动目标。再次按空格键或 enter 键结束操作。",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} 已添加",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "保存时出错。错误:{errorMessage}",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。",
|
||||
"embeddableApi.panel.editPanel.displayName": "编辑 {value}",
|
||||
|
@ -2527,10 +2526,6 @@
|
|||
"embeddableApi.panel.enhancedDashboardPanelAriaLabel": "仪表板面板:{title}",
|
||||
"embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabelWithIndex": "面板 {index} 的选项",
|
||||
"embeddableApi.panel.optionsMenu.panelOptionsButtonEnhancedAriaLabel": "{title} 的面板选项",
|
||||
"embeddableApi.addPanel.createNewDefaultOption": "新建",
|
||||
"embeddableApi.addPanel.displayName": "添加面板",
|
||||
"embeddableApi.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。",
|
||||
"embeddableApi.addPanel.Title": "从库中添加",
|
||||
"embeddableApi.cellValueTrigger.description": "操作在可视化上的单元格值选项中显示",
|
||||
"embeddableApi.cellValueTrigger.title": "单元格值",
|
||||
"embeddableApi.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue