[Embeddable Rebuild] Support grouping in ADD_PANEL_TRIGGER (#179872)

## Summary

Resolves https://github.com/elastic/kibana/issues/179565

Adds support for the `grouping` property in actions assigned to the
`ADD_PANEL_TRIGGER` trigger.

In the "Add panel" context menu, groups from the embeddable factories
and `ADD_PANEL_TRIGGER` actions are merged, allowing us to group legacy
embeddables with the react ones.

### Checklist

- [x] [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
This commit is contained in:
Dima Arnautov 2024-04-05 14:53:25 +02:00 committed by GitHub
parent eb34ad2506
commit bacad3a45d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 376 additions and 62 deletions

View file

@ -21,8 +21,40 @@ describe('getAddPanelActionMenuItems', () => {
isCompatible: () => Promise.resolve(true),
execute: jest.fn(),
},
{
id: 'TEST_ACTION_01',
type: 'TEST_ACTION_01',
getDisplayName: () => 'Action name 01',
getIconType: () => 'pencil',
getDisplayNameTooltip: () => 'Action tooltip',
isCompatible: () => Promise.resolve(true),
execute: jest.fn(),
grouping: [
{
id: 'groupedAddPanelAction',
getDisplayName: () => 'Custom group',
getIconType: () => 'logoElasticsearch',
},
],
},
{
id: 'TEST_ACTION_02',
type: 'TEST_ACTION_02',
getDisplayName: () => 'Action name',
getIconType: () => 'pencil',
getDisplayNameTooltip: () => 'Action tooltip',
isCompatible: () => Promise.resolve(true),
execute: jest.fn(),
grouping: [
{
id: 'groupedAddPanelAction',
getDisplayName: () => 'Custom group',
getIconType: () => 'logoElasticsearch',
},
],
},
];
const items = getAddPanelActionMenuItems(
const [items, grouped] = getAddPanelActionMenuItems(
getMockPresentationContainer(),
registeredActions,
jest.fn()
@ -36,10 +68,38 @@ describe('getAddPanelActionMenuItems', () => {
toolTipContent: 'Action tooltip',
},
]);
expect(grouped).toStrictEqual({
groupedAddPanelAction: {
id: 'groupedAddPanelAction',
title: 'Custom group',
icon: 'logoElasticsearch',
items: [
{
'data-test-subj': 'create-action-Action name 01',
icon: 'pencil',
name: 'Action name 01',
onClick: expect.any(Function),
toolTipContent: 'Action tooltip',
},
{
'data-test-subj': 'create-action-Action name',
icon: 'pencil',
name: 'Action name',
onClick: expect.any(Function),
toolTipContent: 'Action tooltip',
},
],
},
});
});
it('returns empty array if no actions have been registered', async () => {
const items = getAddPanelActionMenuItems(getMockPresentationContainer(), [], jest.fn());
const [items, grouped] = getAddPanelActionMenuItems(
getMockPresentationContainer(),
[],
jest.fn()
);
expect(items).toStrictEqual([]);
expect(grouped).toStrictEqual({});
});
});

View file

@ -7,6 +7,10 @@
*/
import type { ActionExecutionContext, Action } from '@kbn/ui-actions-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import type {
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
import { addPanelMenuTrigger } from '../../triggers';
const onAddPanelActionClick =
@ -26,25 +30,53 @@ const onAddPanelActionClick =
} else action.execute(context);
};
export type GroupedAddPanelActions = EuiContextMenuPanelDescriptor & {
icon?: string;
};
export const getAddPanelActionMenuItems = (
api: PresentationContainer,
actions: Array<Action<object>> | undefined,
closePopover: () => void
) => {
return (
actions?.map((item) => {
const context = {
embeddable: api,
trigger: addPanelMenuTrigger,
};
const actionName = item.getDisplayName(context);
return {
name: actionName,
icon: item.getIconType(context),
onClick: onAddPanelActionClick(item, context, closePopover),
'data-test-subj': `create-action-${actionName}`,
toolTipContent: item?.getDisplayNameTooltip?.(context),
};
}) ?? []
);
): [EuiContextMenuPanelItemDescriptor[], Record<string, GroupedAddPanelActions>] => {
const ungrouped: EuiContextMenuPanelItemDescriptor[] = [];
const grouped: Record<string, GroupedAddPanelActions> = {};
const context = {
embeddable: api,
trigger: addPanelMenuTrigger,
};
const getMenuItem = (item: Action<object>) => {
const actionName = item.getDisplayName(context);
return {
name: actionName,
icon: item.getIconType(context),
onClick: onAddPanelActionClick(item, context, closePopover),
'data-test-subj': `create-action-${actionName}`,
toolTipContent: item?.getDisplayNameTooltip?.(context),
};
};
actions?.forEach((item) => {
if (Array.isArray(item.grouping)) {
item.grouping.forEach((group) => {
if (!grouped[group.id]) {
grouped[group.id] = {
id: group.id,
icon: group.getIconType ? group.getIconType(context) : undefined,
title: group.getDisplayName ? group.getDisplayName(context) : undefined,
items: [],
};
}
grouped[group.id]!.items!.push(getMenuItem(item));
});
} else {
ungrouped.push(getMenuItem(item));
}
});
return [ungrouped, grouped];
};

View file

@ -0,0 +1,148 @@
/*
* 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 { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import { GroupedAddPanelActions } from './add_panel_action_menu_items';
import {
FactoryGroup,
mergeGroupedItemsProvider,
getEmbeddableFactoryMenuItemProvider,
} from './editor_menu';
describe('mergeGroupedItemsProvider', () => {
const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked<PresentationContainer>;
const closePopoverSpy = jest.fn();
const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(
mockApi,
closePopoverSpy
);
const mockFactory = {
id: 'factory1',
type: 'mockFactory',
getDisplayName: () => 'Factory 1',
getDescription: () => 'Factory 1 description',
getIconType: () => 'icon1',
} as unknown as EmbeddableFactory;
const factoryGroupMap = {
group1: {
panelId: 'panel1',
appName: 'App 1',
icon: 'icon1',
factories: [mockFactory],
},
} as unknown as Record<string, FactoryGroup>;
const groupedAddPanelAction = {
group1: {
id: 'panel2',
title: 'Panel 2',
icon: 'icon2',
items: [
{
id: 'addPanelActionId',
},
],
},
} as unknown as Record<string, GroupedAddPanelActions>;
it('should merge factoryGroupMap and groupedAddPanelAction correctly', () => {
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)(factoryGroupMap, groupedAddPanelAction);
expect(initialPanelGroups).toEqual([
{
'data-test-subj': 'dashboardEditorMenu-group1Group',
name: 'App 1',
icon: 'icon1',
panel: 'panel1',
},
]);
expect(additionalPanels).toEqual([
{
id: 'panel1',
title: 'App 1',
items: [
{
icon: 'icon1',
name: 'Factory 1',
toolTipContent: 'Factory 1 description',
'data-test-subj': 'createNew-mockFactory',
onClick: expect.any(Function),
},
{
id: 'addPanelActionId',
},
],
},
]);
});
it('should handle missing factoryGroup correctly', () => {
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)({}, groupedAddPanelAction);
expect(initialPanelGroups).toEqual([
{
'data-test-subj': 'dashboardEditorMenu-group1Group',
name: 'Panel 2',
icon: 'icon2',
panel: 'panel2',
},
]);
expect(additionalPanels).toEqual([
{
id: 'panel2',
title: 'Panel 2',
items: [
{
id: 'addPanelActionId',
},
],
},
]);
});
it('should handle missing groupedAddPanelAction correctly', () => {
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)(factoryGroupMap, {});
expect(initialPanelGroups).toEqual([
{
'data-test-subj': 'dashboardEditorMenu-group1Group',
name: 'App 1',
icon: 'icon1',
panel: 'panel1',
},
]);
expect(additionalPanels).toEqual([
{
id: 'panel1',
title: 'App 1',
items: [
{
icon: 'icon1',
name: 'Factory 1',
toolTipContent: 'Factory 1 description',
'data-test-subj': 'createNew-mockFactory',
onClick: expect.any(Function),
},
],
},
]);
});
});

View file

@ -13,7 +13,8 @@ import {
EuiBadge,
EuiContextMenu,
EuiContextMenuItemIcon,
EuiContextMenuPanelItemDescriptor,
type EuiContextMenuPanelDescriptor,
type EuiContextMenuPanelItemDescriptor,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
@ -27,8 +28,12 @@ import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { pluginServices } from '../../services/plugin_services';
import { DASHBOARD_APP_ID } from '../../dashboard_constants';
import { ADD_PANEL_TRIGGER } from '../../triggers';
import { getAddPanelActionMenuItems } from './add_panel_action_menu_items';
interface FactoryGroup {
import {
getAddPanelActionMenuItems,
type GroupedAddPanelActions,
} from './add_panel_action_menu_items';
export interface FactoryGroup {
id: string;
appName: string;
icon: EuiContextMenuItemIcon;
@ -41,6 +46,97 @@ interface UnwrappedEmbeddableFactory {
isEditable: boolean;
}
export type GetEmbeddableFactoryMenuItem = ReturnType<typeof getEmbeddableFactoryMenuItemProvider>;
export const getEmbeddableFactoryMenuItemProvider =
(api: PresentationContainer, closePopover: () => void) => (factory: EmbeddableFactory) => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';
const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined;
return {
name: factory.getDisplayName(),
icon,
toolTipContent,
onClick: async () => {
closePopover();
api.addNewPanel({ panelType: factory.type }, true);
},
'data-test-subj': `createNew-${factory.type}`,
};
};
export const mergeGroupedItemsProvider =
(getEmbeddableFactoryMenuItem: GetEmbeddableFactoryMenuItem) =>
(
factoryGroupMap: Record<string, FactoryGroup>,
groupedAddPanelAction: Record<string, GroupedAddPanelActions>
): [EuiContextMenuPanelItemDescriptor[], EuiContextMenuPanelDescriptor[]] => {
const initialPanelGroups: EuiContextMenuPanelItemDescriptor[] = [];
const additionalPanels: EuiContextMenuPanelDescriptor[] = [];
new Set(Object.keys(factoryGroupMap).concat(Object.keys(groupedAddPanelAction))).forEach(
(groupId) => {
const dataTestSubj = `dashboardEditorMenu-${groupId}Group`;
const factoryGroup = factoryGroupMap[groupId];
const addPanelGroup = groupedAddPanelAction[groupId];
if (factoryGroup && addPanelGroup) {
const panelId = factoryGroup.panelId;
initialPanelGroups.push({
'data-test-subj': dataTestSubj,
name: factoryGroup.appName,
icon: factoryGroup.icon,
panel: panelId,
});
additionalPanels.push({
id: panelId,
title: factoryGroup.appName,
items: [
...factoryGroup.factories.map(getEmbeddableFactoryMenuItem),
...(addPanelGroup?.items ?? []),
],
});
} else if (factoryGroup) {
const panelId = factoryGroup.panelId;
initialPanelGroups.push({
'data-test-subj': dataTestSubj,
name: factoryGroup.appName,
icon: factoryGroup.icon,
panel: panelId,
});
additionalPanels.push({
id: panelId,
title: factoryGroup.appName,
items: factoryGroup.factories.map(getEmbeddableFactoryMenuItem),
});
} else if (addPanelGroup) {
const panelId = addPanelGroup.id;
initialPanelGroups.push({
'data-test-subj': dataTestSubj,
name: addPanelGroup.title,
icon: addPanelGroup.icon,
panel: panelId,
});
additionalPanels.push({
id: panelId,
title: addPanelGroup.title,
items: addPanelGroup.items,
});
}
}
);
return [initialPanelGroups, additionalPanels];
};
export const EditorMenu = ({
createNewVisType,
isDisabled,
@ -230,44 +326,29 @@ export const EditorMenu = ({
};
};
const getEmbeddableFactoryMenuItem = (
factory: EmbeddableFactory,
closePopover: () => void
): EuiContextMenuPanelItemDescriptor => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';
const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined;
return {
name: factory.getDisplayName(),
icon,
toolTipContent,
onClick: async () => {
closePopover();
api.addNewPanel({ panelType: factory.type }, true);
},
'data-test-subj': `createNew-${factory.type}`,
};
};
const aggsPanelTitle = i18n.translate('dashboard.editorMenu.aggBasedGroupTitle', {
defaultMessage: 'Aggregation based',
});
const getEditorMenuPanels = (closePopover: () => void) => {
const getEditorMenuPanels = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => {
const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closePopover);
const [ungroupedAddPanelActions, groupedAddPanelAction] = getAddPanelActionMenuItems(
api,
addPanelActions,
closePopover
);
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)(factoryGroupMap, groupedAddPanelAction);
const initialPanelItems = [
...visTypeAliases.map(getVisTypeAliasMenuItem),
...getAddPanelActionMenuItems(api, addPanelActions, closePopover),
...ungroupedAddPanelActions,
...toolVisTypes.map(getVisTypeMenuItem),
...ungroupedFactories.map((factory) => {
return getEmbeddableFactoryMenuItem(factory, closePopover);
}),
...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
name: appName,
icon,
panel: panelId,
'data-test-subj': `dashboardEditorMenu-${id}Group`,
})),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...initialPanelGroups,
...promotedVisTypes.map(getVisTypeMenuItem),
];
if (aggsBasedVisTypes.length > 0) {
@ -289,17 +370,10 @@ export const EditorMenu = ({
title: aggsPanelTitle,
items: aggsBasedVisTypes.map(getVisTypeMenuItem),
},
...Object.values(factoryGroupMap).map(
({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({
id: panelId,
title: appName,
items: groupFactories.map((factory) => {
return getEmbeddableFactoryMenuItem(factory, closePopover);
}),
})
),
...additionalPanels,
];
};
return (
<ToolbarPopover
zIndex={Number(euiTheme.levels.header) - 1}