[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:
Nick Peihl 2024-04-24 16:54:15 -04:00 committed by GitHub
parent 6c25f4b07a
commit d0f26c6928
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 349 additions and 114 deletions

View file

@ -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'
);
});
});
});

View file

@ -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 (

View file

@ -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 => {

View file

@ -46,6 +46,7 @@ export {
PANEL_BADGE_TRIGGER,
PANEL_HOVER_TRIGGER,
PANEL_NOTIFICATION_TRIGGER,
registerReactEmbeddableSavedObject,
runEmbeddableFactoryMigrations,
SELECT_RANGE_TRIGGER,
shouldFetch$,

View file

@ -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>]
>;
};

View file

@ -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';

View file

@ -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';

View file

@ -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>

View file

@ -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/**/*"]
}

View file

@ -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());

View file

@ -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;

View file

@ -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.",

View file

@ -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}\"の埋め込み可能ファクトリはすでに登録されています。",

View file

@ -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} 的可嵌入工厂。",