mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
eb34ad2506
commit
bacad3a45d
4 changed files with 376 additions and 62 deletions
|
@ -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({});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue