mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Dashboard] Register React embeddables with Add from Library button (#179289)
Fixes #178545 ## Summary Adds React embeddables to the Add from Library button in Dashboard. Creates a new registry for storing saved object types for embeddables. This registry supercedes the `savedObjectMetaData` property used by class-based legacy embeddable factories. Canvas uses a custom Add from Library button and Canvas does not yet support React embeddables which is out of scope of this PR. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
6c25f4b07a
commit
d0f26c6928
14 changed files with 349 additions and 114 deletions
|
@ -7,14 +7,15 @@
|
|||
*/
|
||||
|
||||
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 { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
|
||||
import { AddPanelFlyout } from './add_panel_flyout';
|
||||
import { core, embeddableStart, usageCollection } from '../kibana_services';
|
||||
import { HelloWorldContainer, ContactCardEmbeddableFactory } from '../lib/test_samples';
|
||||
import { ContactCardEmbeddableFactory } from '../lib/test_samples';
|
||||
import { Container, registerReactEmbeddableSavedObject } from '../lib';
|
||||
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
|
||||
|
||||
// Mock saved objects finder component so we can call the onChoose method.
|
||||
jest.mock('@kbn/saved-objects-finder-plugin/public', () => {
|
||||
|
@ -27,93 +28,168 @@ jest.mock('@kbn/saved-objects-finder-plugin/public', () => {
|
|||
}: {
|
||||
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
|
||||
)
|
||||
}
|
||||
>
|
||||
Dummy Button!
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
id="soFinderAddButton"
|
||||
data-test-subj="soFinderAddButton"
|
||||
onClick={() =>
|
||||
onChoose?.(
|
||||
'awesomeId',
|
||||
'AWESOME_EMBEDDABLE',
|
||||
'Awesome sauce',
|
||||
{} as unknown as SavedObjectCommon
|
||||
)
|
||||
}
|
||||
>
|
||||
Add embeddable!
|
||||
</button>
|
||||
<button
|
||||
id="soFinderAddLegacyButton"
|
||||
data-test-subj="soFinderAddLegacyButton"
|
||||
onClick={() =>
|
||||
onChoose?.(
|
||||
'testId',
|
||||
'CONTACT_CARD_EMBEDDABLE',
|
||||
'test name',
|
||||
{} as unknown as SavedObjectCommon
|
||||
)
|
||||
}
|
||||
>
|
||||
Add legacy embeddable!
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('add panel flyout', () => {
|
||||
let container: HelloWorldContainer;
|
||||
describe('registered embeddables', () => {
|
||||
let container: Container;
|
||||
const onAdd = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
const { overlays } = coreMock.createStart();
|
||||
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
|
||||
(() => null) as any,
|
||||
overlays
|
||||
);
|
||||
beforeAll(() => {
|
||||
registerReactEmbeddableSavedObject({
|
||||
onAdd,
|
||||
embeddableType: 'AWESOME_EMBEDDABLE',
|
||||
savedObjectType: 'AWESOME_EMBEDDABLE',
|
||||
savedObjectName: 'Awesome sauce',
|
||||
getIconForSavedObject: () => 'happyface',
|
||||
});
|
||||
|
||||
embeddableStart.getEmbeddableFactories = jest
|
||||
.fn()
|
||||
.mockReturnValue([contactCardEmbeddableFactory]);
|
||||
embeddableStart.getEmbeddableFactories = jest.fn().mockReturnValue([]);
|
||||
});
|
||||
|
||||
container = new HelloWorldContainer(
|
||||
{
|
||||
id: '1',
|
||||
panels: {},
|
||||
},
|
||||
{
|
||||
getEmbeddableFactory: embeddableStart.getEmbeddableFactory,
|
||||
}
|
||||
);
|
||||
container.addNewEmbeddable = jest.fn().mockResolvedValue({ id: 'foo' });
|
||||
});
|
||||
beforeEach(() => {
|
||||
onAdd.mockClear();
|
||||
container = getMockPresentationContainer() as unknown as Container;
|
||||
// @ts-ignore type is only expected on a dashboard container
|
||||
container.type = 'DASHBOARD_CONTAINER';
|
||||
});
|
||||
|
||||
test('add panel flyout renders SavedObjectFinder', async () => {
|
||||
const component = mount(<AddPanelFlyout container={container} />);
|
||||
test('add panel flyout renders SavedObjectFinder', async () => {
|
||||
const { container: componentContainer } = render(<AddPanelFlyout container={container} />);
|
||||
|
||||
// https://github.com/elastic/kibana/issues/64789
|
||||
expect(component.exists(EuiFlyout)).toBe(false);
|
||||
expect(component.find('#soFinderDummyButton').length).toBe(1);
|
||||
});
|
||||
// component should not contain an extra flyout
|
||||
// https://github.com/elastic/kibana/issues/64789
|
||||
const flyout = componentContainer.querySelector('.euiFlyout');
|
||||
expect(flyout).toBeNull();
|
||||
const dummyButton = screen.queryAllByTestId('soFinderAddButton');
|
||||
expect(dummyButton).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('add panel adds embeddable to container', async () => {
|
||||
const component = mount(<AddPanelFlyout container={container} />);
|
||||
test('add panel calls the registered onAdd method', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(Object.values(container.getInput().panels).length).toBe(0);
|
||||
component.find('#soFinderDummyButton').simulate('click');
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
expect(onAdd).toHaveBeenCalledWith(container, {});
|
||||
});
|
||||
|
||||
expect(container.addNewEmbeddable).toHaveBeenCalled();
|
||||
});
|
||||
test('runs telemetry function on add embeddable', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
|
||||
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(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(core.notifications.toasts.addSuccess).toHaveBeenCalledWith({
|
||||
'data-test-subj': 'addObjectToContainerSuccess',
|
||||
title: 'test name was added',
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'DASHBOARD_CONTAINER',
|
||||
'click',
|
||||
'AWESOME_EMBEDDABLE:add'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('runs telemetry function on add', async () => {
|
||||
const component = mount(<AddPanelFlyout container={container} />);
|
||||
describe('legacy embeddables', () => {
|
||||
let 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));
|
||||
beforeEach(() => {
|
||||
const { overlays } = coreMock.createStart();
|
||||
const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory(
|
||||
(() => null) as any,
|
||||
overlays
|
||||
);
|
||||
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'HELLO_WORLD_CONTAINER',
|
||||
'click',
|
||||
'CONTACT_CARD_EMBEDDABLE:add'
|
||||
);
|
||||
embeddableStart.getEmbeddableFactories = jest
|
||||
.fn()
|
||||
.mockReturnValue([contactCardEmbeddableFactory]);
|
||||
|
||||
container = getMockPresentationContainer() as unknown as Container;
|
||||
container.addNewEmbeddable = jest.fn().mockResolvedValue({ id: 'foo' });
|
||||
// @ts-ignore type is only expected on a dashboard container
|
||||
container.type = 'HELLO_WORLD_CONTAINER';
|
||||
});
|
||||
|
||||
test('add panel flyout renders SavedObjectFinder', async () => {
|
||||
const { container: componentContainer } = render(<AddPanelFlyout container={container} />);
|
||||
|
||||
// component should not contain an extra flyout
|
||||
// https://github.com/elastic/kibana/issues/64789
|
||||
const flyout = componentContainer.querySelector('.euiFlyout');
|
||||
expect(flyout).toBeNull();
|
||||
const dummyButton = screen.queryAllByTestId('soFinderAddLegacyButton');
|
||||
expect(dummyButton).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('add panel adds legacy embeddable to container', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddLegacyButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(container.addNewEmbeddable).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('shows a success toast on add', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddLegacyButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(core.notifications.toasts.addSuccess).toHaveBeenCalledWith({
|
||||
'data-test-subj': 'addObjectToContainerSuccess',
|
||||
title: 'test name was added',
|
||||
});
|
||||
});
|
||||
|
||||
test('runs telemetry function on add legacy embeddable', async () => {
|
||||
render(<AddPanelFlyout container={container} />);
|
||||
|
||||
expect(Object.values(container.children$.value).length).toBe(0);
|
||||
fireEvent.click(screen.getByTestId('soFinderAddLegacyButton'));
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(usageCollection.reportUiCounter).toHaveBeenCalledWith(
|
||||
'HELLO_WORLD_CONTAINER',
|
||||
'click',
|
||||
'CONTACT_CARD_EMBEDDABLE:add'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,32 +9,46 @@
|
|||
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 { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import {
|
||||
SavedObjectFinder,
|
||||
SavedObjectFinderProps,
|
||||
type SavedObjectMetaData,
|
||||
} from '@kbn/saved-objects-finder-plugin/public';
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { apiHasType } from '@kbn/presentation-publishing';
|
||||
import { Toast } from '@kbn/core/public';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import {
|
||||
core,
|
||||
embeddableStart,
|
||||
usageCollection,
|
||||
savedObjectsTaggingOss,
|
||||
contentManagement,
|
||||
usageCollection,
|
||||
} from '../kibana_services';
|
||||
import {
|
||||
IContainer,
|
||||
EmbeddableFactory,
|
||||
SavedObjectEmbeddableInput,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
} from '../lib';
|
||||
import { savedObjectToPanel } from '../registry/saved_object_to_panel_methods';
|
||||
import {
|
||||
ReactEmbeddableSavedObject,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
getReactEmbeddableSavedObjects,
|
||||
PanelIncompatibleError,
|
||||
} from '../lib';
|
||||
|
||||
type FactoryMap = { [key: string]: EmbeddableFactory };
|
||||
type LegacyFactoryMap = { [key: string]: EmbeddableFactory };
|
||||
type FactoryMap<TSavedObjectAttributes extends FinderAttributes = FinderAttributes> = {
|
||||
[key: string]: ReactEmbeddableSavedObject<TSavedObjectAttributes> & { type: string };
|
||||
};
|
||||
|
||||
type CanAddNewEmbeddable = {
|
||||
addNewEmbeddable: (type: string, explicitInput: unknown, attributes: unknown) => { id: string };
|
||||
};
|
||||
|
||||
const apiCanAddNewEmbeddable = (api: unknown): api is CanAddNewEmbeddable => {
|
||||
return typeof (api as CanAddNewEmbeddable).addNewEmbeddable === 'function';
|
||||
};
|
||||
|
||||
let lastToast: string | Toast;
|
||||
const showSuccessToast = (name: string) => {
|
||||
|
@ -52,42 +66,62 @@ const showSuccessToast = (name: string) => {
|
|||
};
|
||||
|
||||
const runAddTelemetry = (
|
||||
parentType: string,
|
||||
factory: EmbeddableFactory,
|
||||
savedObject: SavedObjectCommon
|
||||
parent: unknown,
|
||||
factoryType: string,
|
||||
savedObject: SavedObjectCommon,
|
||||
savedObjectMetaData?: SavedObjectMetaData
|
||||
) => {
|
||||
const type = factory.savedObjectMetaData?.getSavedObjectSubType
|
||||
? factory.savedObjectMetaData.getSavedObjectSubType(savedObject)
|
||||
: factory.type;
|
||||
if (!apiHasType(parent)) return;
|
||||
const type = savedObjectMetaData?.getSavedObjectSubType
|
||||
? savedObjectMetaData.getSavedObjectSubType(savedObject)
|
||||
: factoryType;
|
||||
|
||||
usageCollection?.reportUiCounter?.(parentType, METRIC_TYPE.CLICK, `${type}:add`);
|
||||
usageCollection?.reportUiCounter?.(parent.type, METRIC_TYPE.CLICK, `${type}:add`);
|
||||
};
|
||||
|
||||
export const AddPanelFlyout = ({
|
||||
container,
|
||||
onAddPanel,
|
||||
}: {
|
||||
container: IContainer;
|
||||
container: PresentationContainer;
|
||||
onAddPanel?: (id: string) => void;
|
||||
}) => {
|
||||
const factoriesBySavedObjectType: FactoryMap = useMemo(() => {
|
||||
const legacyFactoriesBySavedObjectType: LegacyFactoryMap = useMemo(() => {
|
||||
return [...embeddableStart.getEmbeddableFactories()]
|
||||
.filter((embeddableFactory) => Boolean(embeddableFactory.savedObjectMetaData?.type))
|
||||
.filter(
|
||||
(embeddableFactory) =>
|
||||
Boolean(embeddableFactory.savedObjectMetaData?.type) && !embeddableFactory.isContainerType
|
||||
)
|
||||
.reduce((acc, factory) => {
|
||||
acc[factory.savedObjectMetaData!.type] = factory;
|
||||
return acc;
|
||||
}, {} as LegacyFactoryMap);
|
||||
}, []);
|
||||
|
||||
const factoriesBySavedObjectType: FactoryMap = useMemo(() => {
|
||||
return [...getReactEmbeddableSavedObjects()]
|
||||
.filter(([type, embeddableFactory]) => {
|
||||
return Boolean(embeddableFactory.savedObjectMetaData?.type);
|
||||
})
|
||||
.reduce((acc, [type, factory]) => {
|
||||
acc[factory.savedObjectMetaData!.type] = {
|
||||
...factory,
|
||||
type,
|
||||
};
|
||||
return acc;
|
||||
}, {} as FactoryMap);
|
||||
}, []);
|
||||
|
||||
const metaData = useMemo(
|
||||
() =>
|
||||
Object.values(factoriesBySavedObjectType)
|
||||
.filter(
|
||||
(embeddableFactory) =>
|
||||
Boolean(embeddableFactory.savedObjectMetaData) && !embeddableFactory.isContainerType
|
||||
)
|
||||
.map(({ savedObjectMetaData }) => savedObjectMetaData as SavedObjectMetaData),
|
||||
[factoriesBySavedObjectType]
|
||||
[
|
||||
...Object.values(factoriesBySavedObjectType),
|
||||
...Object.values(legacyFactoriesBySavedObjectType),
|
||||
]
|
||||
.filter((embeddableFactory) => Boolean(embeddableFactory.savedObjectMetaData))
|
||||
.map(({ savedObjectMetaData }) => savedObjectMetaData!)
|
||||
.sort((a, b) => a.type.localeCompare(b.type)),
|
||||
[factoriesBySavedObjectType, legacyFactoriesBySavedObjectType]
|
||||
);
|
||||
|
||||
const onChoose: SavedObjectFinderProps['onChoose'] = useCallback(
|
||||
|
@ -97,11 +131,26 @@ export const AddPanelFlyout = ({
|
|||
name: string,
|
||||
savedObject: SavedObjectCommon
|
||||
) => {
|
||||
const factoryForSavedObjectType = factoriesBySavedObjectType[type];
|
||||
if (!factoryForSavedObjectType) {
|
||||
if (factoriesBySavedObjectType[type]) {
|
||||
const factory = factoriesBySavedObjectType[type];
|
||||
const { onAdd, savedObjectMetaData } = factory;
|
||||
|
||||
onAdd(container, savedObject);
|
||||
runAddTelemetry(container, factory.type, savedObject, savedObjectMetaData);
|
||||
return;
|
||||
}
|
||||
|
||||
const legacyFactoryForSavedObjectType = legacyFactoriesBySavedObjectType[type];
|
||||
if (!legacyFactoryForSavedObjectType) {
|
||||
throw new EmbeddableFactoryNotFoundError(type);
|
||||
}
|
||||
|
||||
// container.addNewEmbeddable is required for legacy embeddables to support
|
||||
// panel placement strategies
|
||||
if (!apiCanAddNewEmbeddable(container)) {
|
||||
throw new PanelIncompatibleError();
|
||||
}
|
||||
|
||||
let embeddableId: string;
|
||||
|
||||
if (savedObjectToPanel[type]) {
|
||||
|
@ -109,15 +158,15 @@ export const AddPanelFlyout = ({
|
|||
const panel = savedObjectToPanel[type](savedObject);
|
||||
|
||||
const { id: _embeddableId } = await container.addNewEmbeddable(
|
||||
factoryForSavedObjectType.type,
|
||||
legacyFactoryForSavedObjectType.type,
|
||||
panel,
|
||||
savedObject.attributes
|
||||
);
|
||||
|
||||
embeddableId = _embeddableId;
|
||||
} else {
|
||||
const { id: _embeddableId } = await container.addNewEmbeddable<SavedObjectEmbeddableInput>(
|
||||
factoryForSavedObjectType.type,
|
||||
const { id: _embeddableId } = await container.addNewEmbeddable(
|
||||
legacyFactoryForSavedObjectType.type,
|
||||
{ savedObjectId: id },
|
||||
savedObject.attributes
|
||||
);
|
||||
|
@ -128,9 +177,10 @@ export const AddPanelFlyout = ({
|
|||
onAddPanel?.(embeddableId);
|
||||
|
||||
showSuccessToast(name);
|
||||
runAddTelemetry(container.type, factoryForSavedObjectType, savedObject);
|
||||
const { savedObjectMetaData, type: factoryType } = legacyFactoryForSavedObjectType;
|
||||
runAddTelemetry(container, factoryType, savedObject, savedObjectMetaData);
|
||||
},
|
||||
[container, factoriesBySavedObjectType, onAddPanel]
|
||||
[container, factoriesBySavedObjectType, legacyFactoriesBySavedObjectType, onAddPanel]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,7 +12,7 @@ import { OverlayRef } from '@kbn/core/public';
|
|||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { IContainer } from '../lib';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { core } from '../kibana_services';
|
||||
|
||||
const LazyAddPanelFlyout = React.lazy(async () => {
|
||||
|
@ -25,7 +25,7 @@ export const openAddPanelFlyout = ({
|
|||
onAddPanel,
|
||||
onClose,
|
||||
}: {
|
||||
container: IContainer;
|
||||
container: PresentationContainer;
|
||||
onAddPanel?: (id: string) => void;
|
||||
onClose?: () => void;
|
||||
}): OverlayRef => {
|
||||
|
|
|
@ -46,6 +46,7 @@ export {
|
|||
PANEL_BADGE_TRIGGER,
|
||||
PANEL_HOVER_TRIGGER,
|
||||
PANEL_NOTIFICATION_TRIGGER,
|
||||
registerReactEmbeddableSavedObject,
|
||||
runEmbeddableFactoryMigrations,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
shouldFetch$,
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { IconType } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { FinderAttributes, SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public';
|
||||
|
||||
type SOToEmbeddable<TSavedObjectAttributes extends FinderAttributes = FinderAttributes> = (
|
||||
container: PresentationContainer,
|
||||
savedObject: SavedObjectCommon<TSavedObjectAttributes>
|
||||
) => void;
|
||||
|
||||
export type ReactEmbeddableSavedObject<
|
||||
TSavedObjectAttributes extends FinderAttributes = FinderAttributes
|
||||
> = {
|
||||
onAdd: SOToEmbeddable<TSavedObjectAttributes>;
|
||||
savedObjectMetaData: SavedObjectMetaData;
|
||||
};
|
||||
|
||||
const registry: Map<string, ReactEmbeddableSavedObject<any>> = new Map();
|
||||
|
||||
/**
|
||||
* Register an embeddable API saved object with the Add from library flyout.
|
||||
*
|
||||
* @example
|
||||
* registerReactEmbeddableSavedObject({
|
||||
* onAdd: (container, savedObject) => {
|
||||
* container.addNewPanel({
|
||||
* panelType: CONTENT_ID,
|
||||
* initialState: savedObject.attributes,
|
||||
* });
|
||||
* },
|
||||
* embeddableType: CONTENT_ID,
|
||||
* savedObjectType: MAP_SAVED_OBJECT_TYPE,
|
||||
* savedObjectName: i18n.translate('xpack.maps.mapSavedObjectLabel', {
|
||||
* defaultMessage: 'Map',
|
||||
* }),
|
||||
* getIconForSavedObject: () => APP_ICON,
|
||||
* });
|
||||
*/
|
||||
export const registerReactEmbeddableSavedObject = <
|
||||
TSavedObjectAttributes extends FinderAttributes
|
||||
>({
|
||||
onAdd,
|
||||
embeddableType,
|
||||
savedObjectType,
|
||||
savedObjectName,
|
||||
getIconForSavedObject,
|
||||
getSavedObjectSubType,
|
||||
getTooltipForSavedObject,
|
||||
}: {
|
||||
onAdd: SOToEmbeddable<TSavedObjectAttributes>;
|
||||
embeddableType: string;
|
||||
savedObjectType: string;
|
||||
savedObjectName: string;
|
||||
getIconForSavedObject: (savedObject: SavedObjectCommon<TSavedObjectAttributes>) => IconType;
|
||||
getSavedObjectSubType?: (savedObject: SavedObjectCommon<TSavedObjectAttributes>) => string;
|
||||
getTooltipForSavedObject?: (savedObject: SavedObjectCommon<TSavedObjectAttributes>) => string;
|
||||
}) => {
|
||||
if (registry.has(embeddableType)) {
|
||||
throw new Error(
|
||||
i18n.translate('embeddableApi.embeddableSavedObjectRegistry.keyAlreadyExistsError', {
|
||||
defaultMessage: `Embeddable type {embeddableType} already exists in the registry.`,
|
||||
values: { embeddableType },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registry.set(embeddableType, {
|
||||
onAdd,
|
||||
savedObjectMetaData: {
|
||||
name: savedObjectName,
|
||||
type: savedObjectType,
|
||||
getIconForSavedObject,
|
||||
getTooltipForSavedObject,
|
||||
getSavedObjectSubType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getReactEmbeddableSavedObjects = <
|
||||
TSavedObjectAttributes extends FinderAttributes
|
||||
>() => {
|
||||
return registry.entries() as IterableIterator<
|
||||
[string, ReactEmbeddableSavedObject<TSavedObjectAttributes>]
|
||||
>;
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 {
|
||||
type ReactEmbeddableSavedObject,
|
||||
getReactEmbeddableSavedObjects,
|
||||
registerReactEmbeddableSavedObject,
|
||||
} from './embeddable_saved_object_registry';
|
|
@ -16,3 +16,4 @@ export * from './reference_or_value_embeddable';
|
|||
export * from './self_styled_embeddable';
|
||||
export * from './filterable_embeddable';
|
||||
export * from './factory_migrations/run_factory_migrations';
|
||||
export * from './embeddable_saved_object_registry';
|
||||
|
|
|
@ -15,6 +15,10 @@ type SavedObjectToPanelMethod<TSavedObjectAttributes, TByValueInput> = (
|
|||
|
||||
export const savedObjectToPanel: Record<string, SavedObjectToPanelMethod<any, any>> = {};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* React embeddables should register their saved object types with the registerReactEmbeddableSavedObject registry.
|
||||
*/
|
||||
export const registerSavedObjectToPanelMethod = <TSavedObjectAttributes, TByValueAttributes>(
|
||||
savedObjectType: string,
|
||||
method: SavedObjectToPanelMethod<TSavedObjectAttributes, TByValueAttributes>
|
||||
|
|
|
@ -17,19 +17,18 @@
|
|||
"@kbn/es-query",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/i18n",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/std",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/saved-objects-management-plugin",
|
||||
"@kbn/saved-objects-tagging-oss-plugin",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
"@kbn/analytics",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/presentation-panel-plugin",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/analytics",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -196,7 +196,7 @@ export class DashboardAddPanelService extends FtrService {
|
|||
if (filter) {
|
||||
await this.filterEmbeddableNames(filter.replace('-', ' '));
|
||||
}
|
||||
await this.savedObjectsFinder.waitForFilter('Saved search', 'visualization');
|
||||
await this.savedObjectsFinder.waitForFilter('Saved search', 'Visualization');
|
||||
let morePages = true;
|
||||
while (morePages) {
|
||||
searchList.push(await this.addEveryEmbeddableOnCurrentPage());
|
||||
|
|
|
@ -32,7 +32,7 @@ export class SavedObjectsFinderService extends FtrService {
|
|||
for (let i = 0; i < listItems.length; i++) {
|
||||
const listItem = await listItems[i].findByClassName('euiSelectableListItem__text');
|
||||
const text = await listItem.getVisibleText();
|
||||
if (text.includes(type)) {
|
||||
if (text === type) {
|
||||
await listItem.click();
|
||||
await this.toggleFilterPopover();
|
||||
break;
|
||||
|
|
|
@ -2564,7 +2564,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.reactEmbeddable.factoryAlreadyExistsError": "Une usine incorporable pour le type {key} est déjà enregistrée.",
|
||||
|
|
|
@ -2558,7 +2558,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.reactEmbeddable.factoryAlreadyExistsError": "タイプ\"{key}\"の埋め込み可能ファクトリはすでに登録されています。",
|
||||
|
|
|
@ -2566,7 +2566,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.reactEmbeddable.factoryAlreadyExistsError": "已注册类型为 {key} 的可嵌入工厂。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue