Redesign the "Add Panel" Experience (#183764)

## Summary

Closes https://github.com/elastic/kibana/issues/144418

This PR introduces changes to the dashboard add panel selection
functionality, so that panel selection would now happen from within a
flyout, and as such panels are now grouped together logically.

With this implementation any panel that is intended to show up within
this new flyout is required to have either been registered leveraging
the ui action trigger `ADD_PANEL_TRIGGER` and have it's `grouping` value
defined or belong to a subset of visualization types (`PROMOTED`,
`TOOLS`, and `LEGACY`) that would automatically get grouped.

It's worth pointing out that because we can't control the order at which
UI actions gets registered, we won't always get the the panel groups in
the same order, for this specific reason ~a new optional property
(`placementPriority`) has been added in~ the property `order` is now
leveraged such that it allows a user registering a UI action define a
relative weight for where they'd like their group to show up. All
registered actions would be rendered in descending order considering all
`order` defined, in the case where no order is defined `0` is assumed
for the group. In addition an action which is registered without a
group, would automatically get assigned into a default group titled
"Other".

The search implemented within the add panel is rudimentary, checking if
the group titles and group item titles contain the input character; when
a group title is matched the entire group is remains highlighted, in the
case that the group isn't matched and it's just the group item, only
said item is highlighted within it's group.

## Visuals

#### Default view
<img width="2560" alt="Screenshot 2024-06-10 at 17 44 17"
src="90aadf82-684a-4263-aecd-2843c3eff3c1">


#### Search match view
<img width="2560" alt="Screenshot 2024-06-10 at 17 45 11"
src="5a766f29-a3b7-40e3-b1f7-8b423073cd87">


##### P.S.

This changes also includes changes to the display of certain panels;
- ML group has a new title i.e. *Machine Learning and Analytics*
- In serverless, the observability panels (SLO*) only shows as a
selection choice in the observability project type.


### Checklist

Delete any items that are not applicable to this PR.

- [x] 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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] 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)
-->
- [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
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] 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))
- [x] 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))
- [x] 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&mdash;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&mdash;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: Catherine Liu <catherine.liu@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Eyo O. Eyo 2024-06-26 22:55:28 +02:00 committed by GitHub
parent e4b1f02153
commit 22e0545d0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 886 additions and 396 deletions

View file

@ -9,7 +9,11 @@
import { i18n } from '@kbn/i18n';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import {
IncompatibleActionError,
UiActionsStart,
ADD_PANEL_TRIGGER,
} from '@kbn/ui-actions-plugin/public';
import { embeddableExamplesGrouping } from '../embeddable_examples_grouping';
import { ADD_DATA_TABLE_ACTION_ID, DATA_TABLE_ID } from './constants';
@ -39,5 +43,5 @@ export const registerCreateDataTableAction = (uiActions: UiActionsStart) => {
defaultMessage: 'Data table',
}),
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_DATA_TABLE_ACTION_ID);
uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_DATA_TABLE_ACTION_ID);
};

View file

@ -10,4 +10,5 @@ export const embeddableExamplesGrouping = {
id: 'embeddableExamples',
getIconType: () => 'documentation',
getDisplayName: () => 'Embeddable examples',
order: -10,
};

View file

@ -9,7 +9,11 @@
import { i18n } from '@kbn/i18n';
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import {
IncompatibleActionError,
UiActionsStart,
ADD_PANEL_TRIGGER,
} from '@kbn/ui-actions-plugin/public';
import { embeddableExamplesGrouping } from '../embeddable_examples_grouping';
import { ADD_EUI_MARKDOWN_ACTION_ID, EUI_MARKDOWN_ID } from './constants';
import { MarkdownEditorSerializedState } from './types';
@ -41,7 +45,7 @@ export const registerCreateEuiMarkdownAction = (uiActions: UiActionsStart) => {
defaultMessage: 'EUI Markdown',
}),
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_EUI_MARKDOWN_ACTION_ID);
uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_EUI_MARKDOWN_ACTION_ID);
if (uiActions.hasTrigger('ADD_CANVAS_ELEMENT_TRIGGER')) {
// Because Canvas is not enabled in Serverless, this trigger might not be registered - only attach
// the create action if the Canvas-specific trigger does indeed exist.

View file

@ -9,7 +9,7 @@
import { i18n } from '@kbn/i18n';
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin';
import { embeddableExamplesGrouping } from '../embeddable_examples_grouping';
import { ADD_FIELD_LIST_ACTION_ID, FIELD_LIST_ID } from './constants';
@ -34,5 +34,5 @@ export const registerCreateFieldListAction = (uiActions: UiActionsPublicStart) =
defaultMessage: 'Field list',
}),
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_FIELD_LIST_ACTION_ID);
uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_FIELD_LIST_ACTION_ID);
};

View file

@ -10,7 +10,7 @@ import { CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin';
import { embeddableExamplesGrouping } from '../embeddable_examples_grouping';
import {
@ -67,5 +67,5 @@ export const registerCreateSavedBookAction = (uiActions: UiActionsPublicStart, c
defaultMessage: 'Book',
}),
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_SAVED_BOOK_ACTION_ID);
uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_SAVED_BOOK_ACTION_ID);
};

View file

@ -8,7 +8,11 @@
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import {
IncompatibleActionError,
type UiActionsStart,
ADD_PANEL_TRIGGER,
} from '@kbn/ui-actions-plugin/public';
import { embeddableExamplesGrouping } from '../embeddable_examples_grouping';
import { ADD_SEARCH_ACTION_ID, SEARCH_EMBEDDABLE_ID } from './constants';
import { SearchSerializedState } from './types';
@ -33,7 +37,7 @@ export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => {
);
},
});
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_SEARCH_ACTION_ID);
uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_SEARCH_ACTION_ID);
if (uiActions.hasTrigger('ADD_CANVAS_ELEMENT_TRIGGER')) {
// Because Canvas is not enabled in Serverless, this trigger might not be registered - only attach
// the create action if the Canvas-specific trigger does indeed exist.

View file

@ -5,16 +5,18 @@
* 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 type { Trigger } from '@kbn/ui-actions-plugin/public';
import { Trigger } from '.';
export const ADD_PANEL_TRIGGER = 'ADD_PANEL_TRIGGER';
export const addPanelMenuTrigger: Trigger = {
id: ADD_PANEL_TRIGGER,
title: i18n.translate('dashboard.addPanelMenuTrigger.title', {
title: i18n.translate('uiActions.triggers.dashboard.addPanelMenu.title', {
defaultMessage: 'Add panel menu',
}),
description: i18n.translate('dashboard.addPanelMenuTrigger.description', {
description: i18n.translate('uiActions.triggers.dashboard.addPanelMenu.description', {
defaultMessage: "A new action will appear to the dashboard's add panel menu",
}),
};

View file

@ -11,3 +11,4 @@ export * from './row_click_trigger';
export * from './default_trigger';
export * from './visualize_field_trigger';
export * from './visualize_geo_field_trigger';
export * from './dashboard_app_panel_trigger';

View file

@ -8,7 +8,6 @@
import { CoreStart } from '@kbn/core/public';
import { CONTEXT_MENU_TRIGGER, PANEL_NOTIFICATION_TRIGGER } from '@kbn/embeddable-plugin/public';
import { DashboardStartDependencies } from '../plugin';
import { AddToLibraryAction } from './add_to_library_action';
import { LegacyAddToLibraryAction } from './legacy_add_to_library_action';

View file

@ -7,7 +7,7 @@
*/
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
import { getAddPanelActionMenuItems } from './add_panel_action_menu_items';
import { getAddPanelActionMenuItemsGroup } from './add_panel_action_menu_items';
describe('getAddPanelActionMenuItems', () => {
it('returns the items correctly', async () => {
@ -54,39 +54,53 @@ describe('getAddPanelActionMenuItems', () => {
],
},
];
const [items, grouped] = getAddPanelActionMenuItems(
const grouped = getAddPanelActionMenuItemsGroup(
getMockPresentationContainer(),
registeredActions,
jest.fn()
);
expect(items).toStrictEqual([
{
'data-test-subj': 'create-action-Action name',
icon: 'pencil',
name: 'Action name',
onClick: expect.any(Function),
toolTipContent: 'Action tooltip',
},
]);
expect(grouped).toStrictEqual({
groupedAddPanelAction: {
id: 'groupedAddPanelAction',
title: 'Custom group',
icon: 'logoElasticsearch',
order: 0,
'data-test-subj': 'dashboardEditorMenu-groupedAddPanelActionGroup',
items: [
{
'data-test-subj': 'create-action-Action name 01',
icon: 'pencil',
id: 'TEST_ACTION_01',
name: 'Action name 01',
onClick: expect.any(Function),
toolTipContent: 'Action tooltip',
description: 'Action tooltip',
order: 0,
},
{
'data-test-subj': 'create-action-Action name',
icon: 'empty',
id: 'TEST_ACTION_02',
name: 'Action name',
onClick: expect.any(Function),
toolTipContent: 'Action tooltip',
description: 'Action tooltip',
order: 0,
},
],
},
other: {
id: 'other',
title: 'Other',
order: -1,
'data-test-subj': 'dashboardEditorMenu-otherGroup',
items: [
{
id: 'ACTION_CREATE_ESQL_CHART',
name: 'Action name',
icon: 'pencil',
description: 'Action tooltip',
onClick: expect.any(Function),
'data-test-subj': 'create-action-Action name',
order: 0,
},
],
},
@ -94,12 +108,8 @@ describe('getAddPanelActionMenuItems', () => {
});
it('returns empty array if no actions have been registered', async () => {
const [items, grouped] = getAddPanelActionMenuItems(
getMockPresentationContainer(),
[],
jest.fn()
);
expect(items).toStrictEqual([]);
const grouped = getAddPanelActionMenuItemsGroup(getMockPresentationContainer(), [], jest.fn());
expect(grouped).toStrictEqual({});
});
});

View file

@ -5,13 +5,35 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ActionExecutionContext, Action } from '@kbn/ui-actions-plugin/public';
import {
type ActionExecutionContext,
type Action,
addPanelMenuTrigger,
} from '@kbn/ui-actions-plugin/public';
import { PresentationContainer } from '@kbn/presentation-containers';
import type {
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
import { addPanelMenuTrigger } from '../../triggers';
import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import type { IconType, CommonProps } from '@elastic/eui';
import React, { type MouseEventHandler } from 'react';
export interface PanelSelectionMenuItem extends Pick<CommonProps, 'data-test-subj'> {
id: string;
name: string;
icon: IconType;
onClick: MouseEventHandler;
description?: string;
isDisabled?: boolean;
isDeprecated?: boolean;
order: number;
}
export type GroupedAddPanelActions = Pick<
PanelSelectionMenuItem,
'id' | 'isDisabled' | 'data-test-subj' | 'order'
> & {
title: string;
items: PanelSelectionMenuItem[];
};
const onAddPanelActionClick =
(action: Action, context: ActionExecutionContext<object>, closePopover: () => void) =>
@ -30,16 +52,11 @@ const onAddPanelActionClick =
} else action.execute(context);
};
export type GroupedAddPanelActions = EuiContextMenuPanelDescriptor & {
icon?: string;
};
export const getAddPanelActionMenuItems = (
export const getAddPanelActionMenuItemsGroup = (
api: PresentationContainer,
actions: Array<Action<object>> | undefined,
closePopover: () => void
): [EuiContextMenuPanelItemDescriptor[], Record<string, GroupedAddPanelActions>] => {
const ungrouped: EuiContextMenuPanelItemDescriptor[] = [];
) => {
const grouped: Record<string, GroupedAddPanelActions> = {};
const context = {
@ -47,29 +64,31 @@ export const getAddPanelActionMenuItems = (
trigger: addPanelMenuTrigger,
};
const getMenuItem = (item: Action<object>) => {
const getMenuItem = (item: Action<object>): PanelSelectionMenuItem => {
const actionName = item.getDisplayName(context);
return {
id: item.id,
name: actionName,
icon:
(typeof item.getIconType === 'function' ? item.getIconType(context) : undefined) ?? 'empty',
onClick: onAddPanelActionClick(item, context, closePopover),
'data-test-subj': `create-action-${actionName}`,
toolTipContent: item?.getDisplayNameTooltip?.(context),
description: item?.getDisplayNameTooltip?.(context),
order: item.order ?? 0,
};
};
actions?.forEach((item) => {
if (Array.isArray(item.grouping)) {
item.grouping.forEach((group) => {
if (!grouped[group.id]) {
grouped[group.id] = {
id: group.id,
icon:
(typeof group.getIconType === 'function' ? group.getIconType(context) : undefined) ??
'empty',
title: group.getDisplayName ? group.getDisplayName(context) : undefined,
const groupId = group.id;
if (!grouped[groupId]) {
grouped[groupId] = {
id: groupId,
title: group.getDisplayName ? group.getDisplayName(context) : '',
'data-test-subj': `dashboardEditorMenu-${groupId}Group`,
order: group.order ?? 0,
items: [],
};
}
@ -77,9 +96,22 @@ export const getAddPanelActionMenuItems = (
grouped[group.id]!.items!.push(getMenuItem(item));
});
} else {
ungrouped.push(getMenuItem(item));
// use other group as the default for definitions that don't have a group
const fallbackGroup = COMMON_EMBEDDABLE_GROUPING.other;
if (!grouped[fallbackGroup.id]) {
grouped[fallbackGroup.id] = {
id: fallbackGroup.id,
title: fallbackGroup.getDisplayName?.({ embeddable: api }) || '',
'data-test-subj': `dashboardEditorMenu-${fallbackGroup.id}Group`,
order: fallbackGroup.order || 0,
items: [],
};
}
grouped[fallbackGroup.id].items.push(getMenuItem(item));
}
});
return [ungrouped, grouped];
return grouped;
};

View file

@ -3,4 +3,4 @@
@include euiOverflowShadow;
max-height: 60vh;
overflow-y: scroll;
}
}

View file

@ -34,9 +34,10 @@ describe('mergeGroupedItemsProvider', () => {
const factoryGroupMap = {
group1: {
panelId: 'panel1',
id: 'panel1',
appName: 'App 1',
icon: 'icon1',
order: 10,
factories: [mockFactory],
},
} as unknown as Record<string, FactoryGroup>;
@ -46,29 +47,23 @@ describe('mergeGroupedItemsProvider', () => {
id: 'panel2',
title: 'Panel 2',
icon: 'icon2',
order: 10,
items: [
{
id: 'addPanelActionId',
order: 0,
},
],
},
} as unknown as Record<string, GroupedAddPanelActions>;
it('should merge factoryGroupMap and groupedAddPanelAction correctly', () => {
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)(factoryGroupMap, groupedAddPanelAction);
const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)(
factoryGroupMap,
groupedAddPanelAction
);
expect(initialPanelGroups).toEqual([
{
'data-test-subj': 'dashboardEditorMenu-group1Group',
name: 'App 1',
icon: 'icon1',
panel: 'panel1',
},
]);
expect(additionalPanels).toEqual([
expect(groupedPanels).toEqual([
{
id: 'panel1',
title: 'App 1',
@ -76,72 +71,68 @@ describe('mergeGroupedItemsProvider', () => {
{
icon: 'icon1',
name: 'Factory 1',
toolTipContent: 'Factory 1 description',
id: 'mockFactory',
description: 'Factory 1 description',
'data-test-subj': 'createNew-mockFactory',
onClick: expect.any(Function),
order: 0,
},
{
id: 'addPanelActionId',
order: 0,
},
],
'data-test-subj': 'dashboardEditorMenu-group1Group',
order: 10,
},
]);
});
it('should handle missing factoryGroup correctly', () => {
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)({}, groupedAddPanelAction);
const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)(
{},
groupedAddPanelAction
);
expect(initialPanelGroups).toEqual([
{
'data-test-subj': 'dashboardEditorMenu-group1Group',
name: 'Panel 2',
icon: 'icon2',
panel: 'panel2',
},
]);
expect(additionalPanels).toEqual([
expect(groupedPanels).toEqual([
{
id: 'panel2',
icon: 'icon2',
title: 'Panel 2',
items: [
{
id: 'addPanelActionId',
order: 0,
},
],
order: 10,
},
]);
});
it('should handle missing groupedAddPanelAction correctly', () => {
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)(factoryGroupMap, {});
const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)(
factoryGroupMap,
{}
);
expect(initialPanelGroups).toEqual([
{
'data-test-subj': 'dashboardEditorMenu-group1Group',
name: 'App 1',
icon: 'icon1',
panel: 'panel1',
},
]);
expect(additionalPanels).toEqual([
expect(groupedPanels).toEqual([
{
id: 'panel1',
title: 'App 1',
items: [
{
icon: 'icon1',
id: 'mockFactory',
name: 'Factory 1',
toolTipContent: 'Factory 1 description',
description: 'Factory 1 description',
'data-test-subj': 'createNew-mockFactory',
onClick: expect.any(Function),
order: 0,
},
],
order: 10,
'data-test-subj': 'dashboardEditorMenu-group1Group',
},
]);
});

View file

@ -8,37 +8,30 @@
import './editor_menu.scss';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import {
EuiBadge,
EuiContextMenu,
EuiContextMenuItemIcon,
type EuiContextMenuPanelDescriptor,
type EuiContextMenuPanelItemDescriptor,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
} from '@elastic/eui';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { type IconType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { ToolbarPopover } from '@kbn/shared-ux-button-toolbar';
import { type Action, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
import { PresentationContainer } from '@kbn/presentation-containers';
import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { EmbeddableFactory, COMMON_EMBEDDABLE_GROUPING } 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,
getAddPanelActionMenuItemsGroup,
type PanelSelectionMenuItem,
type GroupedAddPanelActions,
} from './add_panel_action_menu_items';
import { openDashboardPanelSelectionFlyout } from './open_dashboard_panel_selection_flyout';
import type { DashboardServices } from '../../services/types';
import { useDashboardAPI } from '../dashboard_app';
export interface FactoryGroup {
id: string;
appName: string;
icon: EuiContextMenuItemIcon;
panelId: number;
icon?: IconType;
factories: EmbeddableFactory[];
order: number;
}
interface UnwrappedEmbeddableFactory {
@ -49,31 +42,38 @@ interface UnwrappedEmbeddableFactory {
export type GetEmbeddableFactoryMenuItem = ReturnType<typeof getEmbeddableFactoryMenuItemProvider>;
export const getEmbeddableFactoryMenuItemProvider =
(api: PresentationContainer, closePopover: () => void) => (factory: EmbeddableFactory) => {
(api: PresentationContainer, closePopover: () => void) =>
(factory: EmbeddableFactory): PanelSelectionMenuItem => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';
const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined;
return {
id: factory.type,
name: factory.getDisplayName(),
icon,
toolTipContent,
description: factory.getDescription?.(),
onClick: async () => {
closePopover();
api.addNewPanel({ panelType: factory.type }, true);
},
'data-test-subj': `createNew-${factory.type}`,
order: factory.order ?? 0,
};
};
const sortGroupPanelsByOrder = <T extends { order: number }>(panelGroups: T[]): T[] => {
return panelGroups.sort(
// larger number sorted to the top
(panelGroupA, panelGroupB) => panelGroupB.order - panelGroupA.order
);
};
export const mergeGroupedItemsProvider =
(getEmbeddableFactoryMenuItem: GetEmbeddableFactoryMenuItem) =>
(
factoryGroupMap: Record<string, FactoryGroup>,
groupedAddPanelAction: Record<string, GroupedAddPanelActions>
): [EuiContextMenuPanelItemDescriptor[], EuiContextMenuPanelDescriptor[]] => {
const initialPanelGroups: EuiContextMenuPanelItemDescriptor[] = [];
const additionalPanels: EuiContextMenuPanelDescriptor[] = [];
) => {
const panelGroups: GroupedAddPanelActions[] = [];
new Set(Object.keys(factoryGroupMap).concat(Object.keys(groupedAddPanelAction))).forEach(
(groupId) => {
@ -83,87 +83,60 @@ export const mergeGroupedItemsProvider =
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,
panelGroups.push({
id: factoryGroup.id,
title: factoryGroup.appName,
'data-test-subj': dataTestSubj,
order: factoryGroup.order,
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,
panelGroups.push({
id: factoryGroup.id,
title: factoryGroup.appName,
'data-test-subj': dataTestSubj,
order: factoryGroup.order,
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,
});
panelGroups.push(addPanelGroup);
}
}
);
return [initialPanelGroups, additionalPanels];
return panelGroups;
};
export const EditorMenu = ({
createNewVisType,
isDisabled,
api,
}: {
interface EditorMenuProps {
api: PresentationContainer;
isDisabled?: boolean;
/** Handler for creating new visualization of a specified type */
createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void;
}) => {
}
export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProps) => {
const isMounted = useRef(false);
const flyoutRef = useRef<ReturnType<DashboardServices['overlays']['openFlyout']>>();
const dashboard = useDashboardAPI();
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
flyoutRef.current?.close();
};
}, []);
const {
embeddable,
visualizations: {
getAliases: getVisTypeAliases,
getByGroup: getVisTypesByGroup,
showNewVisModal,
},
visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup },
uiActions,
} = pluginServices.getServices();
const { euiTheme } = useEuiTheme();
const embeddableFactories = useMemo(
() => Array.from(embeddable.getEmbeddableFactories()),
[embeddable]
);
const [unwrappedEmbeddableFactories, setUnwrappedEmbeddableFactories] = useState<
UnwrappedEmbeddableFactory[]
>([]);
@ -172,6 +145,11 @@ export const EditorMenu = ({
undefined
);
const embeddableFactories = useMemo(
() => Array.from(embeddable.getEmbeddableFactories()),
[embeddable]
);
useEffect(() => {
Promise.all(
embeddableFactories.map<Promise<UnwrappedEmbeddableFactory>>(async (factory) => ({
@ -183,17 +161,6 @@ export const EditorMenu = ({
});
}, [embeddableFactories]);
const createNewAggsBasedVis = useCallback(
(visType?: BaseVisType) => () =>
showNewVisModal({
originatingApp: DASHBOARD_APP_ID,
outsideVisualizeApp: true,
showAggsSelection: true,
selectedVisType: visType,
}),
[showNewVisModal]
);
const getSortedVisTypesByGroup = (group: VisGroups) =>
getVisTypesByGroup(group)
.sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => {
@ -210,8 +177,9 @@ export const EditorMenu = ({
.filter(({ disableCreate }: BaseVisType) => !disableCreate);
const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED);
const aggsBasedVisTypes = getSortedVisTypesByGroup(VisGroups.AGGBASED);
const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS);
const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY);
const visTypeAliases = getVisTypeAliases()
.sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) =>
a === b ? 0 : a ? -1 : 1
@ -224,18 +192,6 @@ export const EditorMenu = ({
);
const factoryGroupMap: Record<string, FactoryGroup> = {};
const ungroupedFactories: EmbeddableFactory[] = [];
const aggBasedPanelID = 1;
let panelCount = 1 + aggBasedPanelID;
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Retrieve ADD_PANEL_TRIGGER actions
useEffect(() => {
@ -243,6 +199,7 @@ export const EditorMenu = ({
const registeredActions = await uiActions?.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, {
embeddable: api,
});
if (isMounted.current) {
setAddPanelActions(registeredActions);
}
@ -260,142 +217,160 @@ export const EditorMenu = ({
} else {
factoryGroupMap[group.id] = {
id: group.id,
appName: group.getDisplayName ? group.getDisplayName({ embeddable }) : group.id,
icon: (group.getIconType
? group.getIconType({ embeddable })
: 'empty') as EuiContextMenuItemIcon,
appName: group.getDisplayName
? group.getDisplayName({ embeddable: dashboard })
: group.id,
icon: group.getIconType?.({ embeddable: dashboard }),
factories: [factory],
panelId: panelCount,
order: group.order ?? 0,
};
panelCount++;
}
});
} else {
ungroupedFactories.push(factory);
const fallbackGroup = COMMON_EMBEDDABLE_GROUPING.other;
if (!factoryGroupMap[fallbackGroup.id]) {
factoryGroupMap[fallbackGroup.id] = {
id: fallbackGroup.id,
appName: fallbackGroup.getDisplayName
? fallbackGroup.getDisplayName({ embeddable: dashboard })
: fallbackGroup.id,
icon: fallbackGroup.getIconType?.({ embeddable: dashboard }) || 'empty',
factories: [],
order: fallbackGroup.order ?? 0,
};
}
factoryGroupMap[fallbackGroup.id].factories.push(factory);
}
});
const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => {
const augmentedCreateNewVisType = (
visType: Parameters<EditorMenuProps['createNewVisType']>[0],
cb: () => void
) => {
const visClickHandler = createNewVisType(visType);
return () => {
visClickHandler();
cb();
};
};
const getVisTypeMenuItem = (
onClickCb: () => void,
visType: BaseVisType
): PanelSelectionMenuItem => {
const {
name,
title,
titleInWizard,
description,
icon = 'empty',
group,
isDeprecated,
order,
} = visType;
return {
name: !isDeprecated ? (
titleInWizard || title
) : (
<EuiFlexGroup wrap responsive={false} gutterSize="s">
<EuiFlexItem grow={false}>{titleInWizard || title}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
{i18n.translate('dashboard.editorMenu.deprecatedTag', {
defaultMessage: 'Deprecated',
})}
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
),
icon: icon as string,
onClick:
// not all the agg-based visualizations need to be created via the wizard
group === VisGroups.AGGBASED && visType.options.showIndexSelection
? createNewAggsBasedVis(visType)
: createNewVisType(visType),
id: name,
name: titleInWizard || title,
isDeprecated,
icon,
onClick: augmentedCreateNewVisType(visType, onClickCb),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
description,
order,
};
};
const getVisTypeAliasMenuItem = (
onClickCb: () => void,
visTypeAlias: VisTypeAlias
): EuiContextMenuPanelItemDescriptor => {
const { name, title, description, icon = 'empty' } = visTypeAlias;
): PanelSelectionMenuItem => {
const { name, title, description, icon = 'empty', order } = visTypeAlias;
return {
id: name,
name: title,
icon,
onClick: createNewVisType(visTypeAlias),
onClick: augmentedCreateNewVisType(visTypeAlias, onClickCb),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
description,
order: order ?? 0,
};
};
const aggsPanelTitle = i18n.translate('dashboard.editorMenu.aggBasedGroupTitle', {
defaultMessage: 'Aggregation based',
});
const getEditorMenuPanels = (closeFlyout: () => void): GroupedAddPanelActions[] => {
const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closeFlyout);
const getEditorMenuPanels = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => {
const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closePopover);
const [ungroupedAddPanelActions, groupedAddPanelAction] = getAddPanelActionMenuItems(
const groupedAddPanelAction = getAddPanelActionMenuItemsGroup(
api,
addPanelActions,
closePopover
closeFlyout
);
const [initialPanelGroups, additionalPanels] = mergeGroupedItemsProvider(
getEmbeddableFactoryMenuItem
)(factoryGroupMap, groupedAddPanelAction);
const initialPanelGroups = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)(
factoryGroupMap,
groupedAddPanelAction
);
const initialPanelItems = [
...visTypeAliases.map(getVisTypeAliasMenuItem),
...ungroupedAddPanelActions,
...toolVisTypes.map(getVisTypeMenuItem),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...initialPanelGroups,
...promotedVisTypes.map(getVisTypeMenuItem),
];
if (aggsBasedVisTypes.length > 0) {
initialPanelItems.push({
name: aggsPanelTitle,
icon: 'visualizeApp',
panel: aggBasedPanelID,
'data-test-subj': `dashboardEditorAggBasedMenuItem`,
});
}
return [
{
id: 0,
items: initialPanelItems,
},
{
id: aggBasedPanelID,
title: aggsPanelTitle,
items: aggsBasedVisTypes.map(getVisTypeMenuItem),
},
...additionalPanels,
];
// enhance panel groups
return sortGroupPanelsByOrder<GroupedAddPanelActions>(initialPanelGroups).map((panelGroup) => {
switch (panelGroup.id) {
case 'visualizations': {
return {
...panelGroup,
items: sortGroupPanelsByOrder<PanelSelectionMenuItem>(
(panelGroup.items ?? []).concat(
// TODO: actually add grouping to vis type alias so we wouldn't randomly display an unintended item
visTypeAliases.map(getVisTypeAliasMenuItem.bind(null, closeFlyout)),
promotedVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout))
)
),
};
}
case COMMON_EMBEDDABLE_GROUPING.legacy.id: {
return {
...panelGroup,
items: sortGroupPanelsByOrder<PanelSelectionMenuItem>(
(panelGroup.items ?? []).concat(
legacyVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout))
)
),
};
}
case COMMON_EMBEDDABLE_GROUPING.annotation.id: {
return {
...panelGroup,
items: sortGroupPanelsByOrder<PanelSelectionMenuItem>(
(panelGroup.items ?? []).concat(
toolVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout))
)
),
};
}
default: {
return {
...panelGroup,
items: sortGroupPanelsByOrder(panelGroup.items),
};
}
}
});
};
return (
<ToolbarPopover
zIndex={Number(euiTheme.levels.header) - 1}
repositionOnScroll
ownFocus
<ToolbarButton
data-test-subj="dashboardEditorMenuButton"
isDisabled={isDisabled}
iconType="plusInCircle"
label={i18n.translate('dashboard.solutionToolbar.editorMenuButtonLabel', {
defaultMessage: 'Add panel',
})}
isDisabled={isDisabled}
onClick={() => {
flyoutRef.current = openDashboardPanelSelectionFlyout({
getPanels: getEditorMenuPanels,
});
}}
size="s"
iconType="plusInCircle"
panelPaddingSize="none"
data-test-subj="dashboardEditorMenuButton"
>
{({ closePopover }: { closePopover: () => void }) => (
<EuiContextMenu
initialPanelId={0}
panels={getEditorMenuPanels(closePopover)}
className={`dshSolutionToolbar__editorContextMenu`}
data-test-subj="dashboardEditorContextMenu"
/>
)}
</ToolbarPopover>
/>
);
};

View file

@ -0,0 +1,255 @@
/*
* 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, { useEffect, useState, useRef } from 'react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { i18n as i18nFn } from '@kbn/i18n';
import orderBy from 'lodash/orderBy';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiForm,
EuiBadge,
EuiFormRow,
EuiTitle,
EuiFieldSearch,
useEuiTheme,
type EuiFlyoutProps,
EuiListGroup,
EuiListGroupItem,
EuiToolTip,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { pluginServices } from '../../services/plugin_services';
import type { DashboardServices } from '../../services/types';
import type { GroupedAddPanelActions, PanelSelectionMenuItem } from './add_panel_action_menu_items';
interface OpenDashboardPanelSelectionFlyoutArgs {
getPanels: (closePopover: () => void) => GroupedAddPanelActions[];
flyoutPanelPaddingSize?: Exclude<EuiFlyoutProps['paddingSize'], 'none'>;
}
interface Props extends Pick<OpenDashboardPanelSelectionFlyoutArgs, 'getPanels'> {
/** Handler to close flyout */
close: () => void;
/** Padding for flyout */
paddingSize: Exclude<OpenDashboardPanelSelectionFlyoutArgs['flyoutPanelPaddingSize'], undefined>;
}
export function openDashboardPanelSelectionFlyout({
getPanels,
flyoutPanelPaddingSize = 'l',
}: OpenDashboardPanelSelectionFlyoutArgs) {
const {
overlays,
analytics,
settings: { i18n, theme },
} = pluginServices.getServices();
// eslint-disable-next-line prefer-const
let flyoutRef: ReturnType<DashboardServices['overlays']['openFlyout']>;
const mount = toMountPoint(
React.createElement(function () {
const closeFlyout = () => flyoutRef.close();
return (
<DashboardPanelSelectionListFlyout
close={closeFlyout}
{...{ paddingSize: flyoutPanelPaddingSize, getPanels }}
/>
);
}),
{ analytics, theme, i18n }
);
flyoutRef = overlays.openFlyout(mount, {
size: 'm',
maxWidth: 500,
paddingSize: flyoutPanelPaddingSize,
'aria-labelledby': 'addPanelsFlyout',
'data-test-subj': 'dashboardPanelSelectionFlyout',
});
return flyoutRef;
}
export const DashboardPanelSelectionListFlyout: React.FC<Props> = ({
close,
getPanels,
paddingSize,
}) => {
const { euiTheme } = useEuiTheme();
const panels = useRef(getPanels(close));
const [searchTerm, setSearchTerm] = useState<string>('');
const [panelsSearchResult, setPanelsSearchResult] = useState<GroupedAddPanelActions[]>(
panels.current
);
useEffect(() => {
if (!searchTerm) {
return setPanelsSearchResult(panels.current);
}
const q = searchTerm.toLowerCase();
setPanelsSearchResult(
orderBy(
panels.current.map((panel) => {
const groupSearchMatch = panel.title.toLowerCase().includes(q);
const [groupSearchMatchAgg, items] = panel.items.reduce(
(acc, cur) => {
const searchMatch = cur.name.toLowerCase().includes(q);
acc[0] = acc[0] || searchMatch;
acc[1].push({
...cur,
isDisabled: !(groupSearchMatch || searchMatch),
});
return acc;
},
[groupSearchMatch, [] as PanelSelectionMenuItem[]]
);
return {
...panel,
isDisabled: !groupSearchMatchAgg,
items,
};
}),
['isDisabled']
)
);
}, [searchTerm]);
return (
<>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h1 id="addPanelsFlyout">
<FormattedMessage
id="dashboard.solutionToolbar.addPanelFlyout.headingText"
defaultMessage="Add panel"
/>
</h1>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlexGroup direction="column" responsive={false} gutterSize="m">
<EuiFlexItem
grow={false}
css={{
position: 'sticky',
top: euiTheme.size[paddingSize],
zIndex: 1,
boxShadow: `0 -${euiTheme.size[paddingSize]} 0 4px ${euiTheme.colors.emptyShade}`,
}}
>
<EuiForm component="form" fullWidth>
<EuiFormRow css={{ backgroundColor: euiTheme.colors.emptyShade }}>
<EuiFieldSearch
autoFocus
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
}}
aria-label={i18nFn.translate(
'dashboard.editorMenu.addPanelFlyout.searchLabelText',
{ defaultMessage: 'search field for panels' }
)}
className="nsPanelSelectionFlyout__searchInput"
data-test-subj="dashboardPanelSelectionFlyout__searchInput"
/>
</EuiFormRow>
</EuiForm>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" gutterSize="m">
{panelsSearchResult.some(({ isDisabled }) => !isDisabled) ? (
panelsSearchResult.map(
({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj }) =>
!isDisabled ? (
<EuiFlexItem key={id} data-test-subj={dataTestSubj}>
<EuiTitle id={`${id}-group`} size="xxs">
{typeof title === 'string' ? <h3>{title}</h3> : title}
</EuiTitle>
<EuiListGroup
aria-labelledby={`${id}-group`}
size="s"
gutterSize="none"
maxWidth={false}
flush
>
{items?.map((item, idx) => {
return (
<EuiListGroupItem
key={`${id}.${idx}`}
label={
<EuiToolTip position="right" content={item.description}>
{!item.isDeprecated ? (
<EuiText size="s">{item.name}</EuiText>
) : (
<EuiFlexGroup wrap responsive={false} gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="s">{item.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
<FormattedMessage
id="dashboard.editorMenu.deprecatedTag"
defaultMessage="Deprecated"
/>
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiToolTip>
}
onClick={item?.onClick}
iconType={item.icon}
data-test-subj={item['data-test-subj']}
isDisabled={item.isDisabled}
/>
);
})}
</EuiListGroup>
</EuiFlexItem>
) : null
)
) : (
<EuiText size="s" textAlign="center">
<FormattedMessage
id="dashboard.solutionToolbar.addPanelFlyout.noResultsDescription"
defaultMessage="No panel types found"
/>
</EuiText>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={close}>
<FormattedMessage
id="dashboard.solutionToolbar.addPanelFlyout.cancelButtonText"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

View file

@ -30,6 +30,7 @@ import type {
UsageCollectionStart,
} from '@kbn/usage-collection-plugin/public';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { type UiActionsSetup, type UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
@ -39,7 +40,6 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public';
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
@ -70,7 +70,6 @@ import {
import { DashboardMountContextProps } from './dashboard_app/types';
import type { FindDashboardsService } from './services/dashboard_content_management/types';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
import { addPanelMenuTrigger } from './triggers';
import { GetPanelPlacementSettings } from './dashboard_container/panel_placement';
export interface DashboardFeatureFlagConfig {
@ -167,10 +166,6 @@ export class DashboardPlugin
this.dashboardFeatureFlagConfig =
this.initializerContext.config.get<DashboardFeatureFlagConfig>();
// this trigger enables external consumers to register actions for
// adding items to the add panel menu
uiActions.registerTrigger(addPanelMenuTrigger);
core.analytics.registerEventType({
eventType: 'dashboard_loaded_with_data',
schema: {},

View file

@ -108,3 +108,5 @@ export {
embeddableInputToSubject,
embeddableOutputToSubject,
} from './lib/embeddables/compatibility/embeddable_compatibility_utils';
export { COMMON_EMBEDDABLE_GROUPING } from './lib/embeddables/common/constants';

View file

@ -0,0 +1,37 @@
/*
* 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 { UiActionsPresentableGroup } from '@kbn/ui-actions-plugin/public';
export const COMMON_EMBEDDABLE_GROUPING: { [key: string]: UiActionsPresentableGroup<unknown> } = {
legacy: {
id: 'legacy',
getDisplayName: () =>
i18n.translate('embeddableApi.common.constants.grouping.legacy', {
defaultMessage: 'Legacy',
}),
order: -2,
},
annotation: {
id: 'annotation-and-navigation',
getDisplayName: () =>
i18n.translate('embeddableApi.common.constants.grouping.annotations', {
defaultMessage: 'Annotations and Navigation',
}),
},
other: {
id: 'other',
getDisplayName: () =>
i18n.translate('embeddableApi.common.constants.grouping.other', {
defaultMessage: 'Other',
}),
getIconType: () => 'empty',
order: -1,
},
};

View file

@ -148,4 +148,6 @@ export interface EmbeddableFactory<
initialInput: TEmbeddableInput,
parent?: IContainer
): Promise<TEmbeddable | ErrorEmbeddable | undefined>;
order?: number;
}

View file

@ -9,7 +9,8 @@
import { i18n } from '@kbn/i18n';
import { CanAddNewPanel } from '@kbn/presentation-containers';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import {
ADD_IMAGE_EMBEDDABLE_ACTION_ID,
IMAGE_EMBEDDABLE_TYPE,
@ -27,6 +28,7 @@ export const registerCreateImageAction = () => {
uiActionsService.registerAction<EmbeddableApiContext>({
id: ADD_IMAGE_EMBEDDABLE_ACTION_ID,
getIconType: () => 'image',
order: 20,
isCompatible: async ({ embeddable: parentApi }) => {
return Boolean(await parentApiIsCompatible(parentApi));
},
@ -45,13 +47,14 @@ export const registerCreateImageAction = () => {
// swallow the rejection, since this just means the user closed without saving
}
},
grouping: [COMMON_EMBEDDABLE_GROUPING.annotation],
getDisplayName: () =>
i18n.translate('imageEmbeddable.imageEmbeddableFactory.displayName', {
defaultMessage: 'Image',
}),
});
uiActionsService.attachAction('ADD_PANEL_TRIGGER', ADD_IMAGE_EMBEDDABLE_ACTION_ID);
uiActionsService.attachAction(ADD_PANEL_TRIGGER, ADD_IMAGE_EMBEDDABLE_ACTION_ID);
if (uiActionsService.hasTrigger('ADD_CANVAS_ELEMENT_TRIGGER')) {
// Because Canvas is not enabled in Serverless, this trigger might not be registered - only attach
// the create action if the Canvas-specific trigger does indeed exist.

View file

@ -17,6 +17,7 @@ import {
Embeddable,
ReferenceOrValueEmbeddable,
SavedObjectEmbeddableInput,
COMMON_EMBEDDABLE_GROUPING,
} from '@kbn/embeddable-plugin/public';
import { CONTENT_ID } from '../../common';
@ -44,6 +45,8 @@ export class LinksEmbeddable
public attributes?: LinksAttributes;
public attributes$ = new Subject<LinksAttributes>();
public grouping = [COMMON_EMBEDDABLE_GROUPING.annotation];
constructor(
config: LinksConfig,
initialInput: LinksInput,

View file

@ -14,6 +14,7 @@ import {
EmbeddableFactory,
EmbeddableFactoryDefinition,
ErrorEmbeddable,
COMMON_EMBEDDABLE_GROUPING,
} from '@kbn/embeddable-plugin/public';
import {
GetMigrationFunctionObjectFn,
@ -55,7 +56,8 @@ export class LinksFactoryDefinition
| ((state: EmbeddableStateWithType, stats: Record<string, any>) => Record<string, any>)
| undefined;
migrations?: MigrateFunctionsObject | GetMigrationFunctionObjectFn | undefined;
grouping?: UiActionsPresentableGrouping<unknown> | undefined;
grouping: UiActionsPresentableGrouping<unknown> = [COMMON_EMBEDDABLE_GROUPING.annotation];
public readonly type = CONTENT_ID;
public readonly isContainerType = false;

View file

@ -24,6 +24,7 @@ export { ActionInternal, createAction, IncompatibleActionError } from './actions
export { buildContextMenuForActions } from './context_menu';
export type {
Presentable as UiActionsPresentable,
PresentableGroup as UiActionsPresentableGroup,
PresentableGrouping as UiActionsPresentableGrouping,
} from '@kbn/ui-actions-browser/src/types';
export type { Trigger, RowClickContext } from '@kbn/ui-actions-browser/src/triggers';
@ -34,6 +35,8 @@ export {
visualizeGeoFieldTrigger,
ROW_CLICK_TRIGGER,
rowClickTrigger,
ADD_PANEL_TRIGGER,
addPanelMenuTrigger,
} from '@kbn/ui-actions-browser/src/triggers';
export type { VisualizeFieldContext } from './types';
export {

View file

@ -12,6 +12,7 @@ import {
rowClickTrigger,
visualizeFieldTrigger,
visualizeGeoFieldTrigger,
addPanelMenuTrigger,
} from '@kbn/ui-actions-browser/src/triggers';
import { UiActionsService } from './service';
import { setAnalytics, setI18n, setTheme } from './services';
@ -48,6 +49,7 @@ export class UiActionsPlugin
constructor(_initializerContext: PluginInitializerContext) {}
public setup(_core: CoreSetup): UiActionsPublicSetup {
this.service.registerTrigger(addPanelMenuTrigger);
this.service.registerTrigger(rowClickTrigger);
this.service.registerTrigger(visualizeFieldTrigger);
this.service.registerTrigger(visualizeGeoFieldTrigger);

View file

@ -27,6 +27,7 @@ export const markdownVisDefinition: VisTypeDefinition<MarkdownVisParams> = {
description: i18n.translate('visTypeMarkdown.markdownDescription', {
defaultMessage: 'Add text and images to your dashboard.',
}),
order: 30,
toExpressionAst,
visConfig: {
defaults: {

View file

@ -104,7 +104,8 @@ export const metricsVisDefinition: VisTypeDefinition<
defaultMessage: 'Perform advanced analysis of your time series data.',
}),
icon: 'visVisualBuilder',
group: VisGroups.PROMOTED,
group: VisGroups.LEGACY,
order: 10,
visConfig: {
defaults: {
id: () => uuidv4(),

View file

@ -0,0 +1,64 @@
/*
* 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 {
apiHasAppContext,
EmbeddableApiContext,
HasType,
HasAppContext,
} from '@kbn/presentation-publishing';
import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { apiHasType } from '@kbn/presentation-publishing';
import { apiCanAddNewPanel, CanAddNewPanel } from '@kbn/presentation-containers';
import { showNewVisModal } from '../wizard/show_new_vis';
const ADD_AGG_VIS_ACTION_ID = 'ADD_AGG_VIS';
type AddAggVisualizationPanelActionApi = HasType & CanAddNewPanel & HasAppContext;
const isApiCompatible = (api: unknown | null): api is AddAggVisualizationPanelActionApi => {
return apiHasType(api) && apiCanAddNewPanel(api) && apiHasAppContext(api);
};
export class AddAggVisualizationPanelAction implements Action<EmbeddableApiContext> {
public readonly type = ADD_AGG_VIS_ACTION_ID;
public readonly id = ADD_AGG_VIS_ACTION_ID;
public readonly grouping = [COMMON_EMBEDDABLE_GROUPING.legacy];
public readonly order = 20;
constructor() {}
public getIconType() {
return 'visualizeApp';
}
public getDisplayName() {
return i18n.translate('visualizations.uiAction.addAggVis.displayName', {
defaultMessage: 'Aggregation based',
});
}
public async isCompatible({ embeddable }: EmbeddableApiContext) {
return isApiCompatible(embeddable);
}
public async execute({ embeddable }: EmbeddableApiContext): Promise<void> {
if (!isApiCompatible(embeddable)) {
throw new IncompatibleActionError();
}
showNewVisModal({
originatingApp: embeddable.getAppContext().currentAppId,
outsideVisualizeApp: true,
showAggsSelection: true,
});
}
}

View file

@ -7,3 +7,14 @@
*/
export { VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants';
export const COMMON_VISUALIZATION_GROUPING = [
{
id: 'visualizations',
getDisplayName: () => 'Visualizations',
getIconType: () => {
return 'visGauge';
},
order: 1000,
},
];

View file

@ -7,7 +7,7 @@
*/
export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory';
export { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
export { VISUALIZE_EMBEDDABLE_TYPE, COMMON_VISUALIZATION_GROUPING } from './constants';
export { VIS_EVENT_TO_TRIGGER } from './events';
export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object';

View file

@ -21,6 +21,7 @@ export {
apiHasVisualizeConfig,
VISUALIZE_EMBEDDABLE_TYPE,
VIS_EVENT_TO_TRIGGER,
COMMON_VISUALIZATION_GROUPING,
} from './embeddable';
export { VisualizationContainer } from './components';
export { getVisSchemas } from './vis_schemas';

View file

@ -36,7 +36,7 @@ import type {
ApplicationStart,
SavedObjectsClientContract,
} from '@kbn/core/public';
import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { UiActionsStart, UiActionsSetup, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type {
@ -47,7 +47,11 @@ import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import {
CONTEXT_MENU_TRIGGER,
EmbeddableSetup,
EmbeddableStart,
} from '@kbn/embeddable-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
@ -122,6 +126,7 @@ import {
} from '../common/content_management';
import { SerializedVisData } from '../common';
import { VisualizeByValueInput } from './embeddable/visualize_embeddable';
import { AddAggVisualizationPanelAction } from './actions/add_agg_vis_action';
/**
* Interface for this plugin's returned setup/start contracts.
@ -394,7 +399,9 @@ export class VisualizationsPlugin
uiActions.registerTrigger(visualizeEditorTrigger);
uiActions.registerTrigger(dashboardVisualizationPanelTrigger);
const editInLensAction = new EditInLensAction(data.query.timefilter.timefilter);
uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', editInLensAction);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editInLensAction);
const addAggVisAction = new AddAggVisualizationPanelAction();
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addAggVisAction);
const embeddableFactory = new VisualizeEmbeddableFactory({ start });
embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory);

View file

@ -24,6 +24,7 @@ const defaultOptions: VisTypeOptions = {
export class BaseVisType<TVisParams extends VisParams = VisParams> {
public readonly name;
public readonly title;
public readonly order;
public readonly description;
public readonly note;
public readonly getSupportedTriggers;
@ -67,6 +68,7 @@ export class BaseVisType<TVisParams extends VisParams = VisParams> {
this.title = opts.title;
this.icon = opts.icon;
this.image = opts.image;
this.order = opts.order ?? 0;
this.suppressWarnings = opts.suppressWarnings;
this.visConfig = defaultsDeep({}, opts.visConfig, { defaults: {} });
this.editorConfig = defaultsDeep({}, opts.editorConfig, { collections: {} });

View file

@ -217,4 +217,6 @@ export interface VisTypeDefinition<TVisParams extends VisParams> {
* have incosistencies in legacy visLib visualizations
*/
readonly visConfig: Record<string, any>;
readonly order?: number;
}

View file

@ -10,4 +10,5 @@ export enum VisGroups {
PROMOTED = 'promoted',
TOOLS = 'tools',
AGGBASED = 'aggbased',
LEGACY = 'legacy',
}

View file

@ -117,6 +117,7 @@ export interface VisTypeAlias {
visualizations: VisualizationsAppExtension;
[appName: string]: unknown;
};
order?: number;
}
let registry: VisTypeAlias[] = [];

View file

@ -35,6 +35,7 @@ interface VisTypeListEntry {
}
interface AggBasedSelectionProps {
openedAsRoot?: boolean;
onVisTypeSelected: (visType: BaseVisType) => void;
visTypesRegistry: TypesStart;
toggleGroups: (flag: boolean) => void;
@ -58,13 +59,15 @@ class AggBasedSelection extends React.Component<AggBasedSelectionProps, AggBased
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="visualizations.newVisWizard.title"
defaultMessage="New visualization"
id="visualizations.newAggVisWizard.title"
defaultMessage="New aggregation based visualization"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<DialogNavigation goBack={() => this.props.toggleGroups(true)} />
{this.props.openedAsRoot ? null : (
<DialogNavigation goBack={() => this.props.toggleGroups(true)} />
)}
<EuiFieldSearch
placeholder="Filter"
value={query}

View file

@ -57,6 +57,8 @@ function GroupSelection(props: GroupSelectionProps) {
[
...props.visTypesRegistry.getAliases(),
...props.visTypesRegistry.getByGroup(VisGroups.PROMOTED),
// Include so TSVB still gets displayed
...props.visTypesRegistry.getByGroup(VisGroups.LEGACY),
].filter((visDefinition) => {
return !visDefinition.disableCreate;
}),
@ -65,6 +67,7 @@ function GroupSelection(props: GroupSelectionProps) {
),
[props.visTypesRegistry]
);
return (
<>
<EuiModalHeader>

View file

@ -106,6 +106,7 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
visTypesRegistry={this.props.visTypesRegistry}
docLinks={this.props.docLinks}
toggleGroups={(flag: boolean) => this.setState({ showGroups: flag })}
openedAsRoot={this.props.showAggsSelection && !this.props.selectedVisType}
/>
</EuiModal>
);

View file

@ -7,9 +7,8 @@
*/
import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom';
import { EuiPortal, EuiProgress } from '@elastic/eui';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { toMountPoint } from '@kbn/react-kibana-mount';
import {
getHttp,
getTypes,
@ -50,47 +49,54 @@ export function showNewVisModal({
selectedVisType,
}: ShowNewVisModalParams = {}) {
const container = document.createElement('div');
let isClosed = false;
// initialize variable that will hold reference for unmount
// eslint-disable-next-line prefer-const
let unmount: ReturnType<ReturnType<typeof toMountPoint>>;
const handleClose = () => {
if (isClosed) return;
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
if (onClose) {
onClose();
}
onClose?.();
unmount?.();
isClosed = true;
};
document.body.appendChild(container);
const element = (
<KibanaRenderContextProvider analytics={getAnalytics()} i18n={getI18n()} theme={getTheme()}>
<Suspense
fallback={
<EuiPortal>
<EuiProgress size="xs" position="fixed" />
</EuiPortal>
}
>
<NewVisModal
isOpen={true}
onClose={handleClose}
originatingApp={originatingApp}
stateTransfer={getEmbeddable().getStateTransfer()}
outsideVisualizeApp={outsideVisualizeApp}
editorParams={editorParams}
visTypesRegistry={getTypes()}
contentClient={getContentManagement().client}
uiSettings={getUISettings()}
addBasePath={getHttp().basePath.prepend}
application={getApplication()}
docLinks={getDocLinks()}
showAggsSelection={showAggsSelection}
selectedVisType={selectedVisType}
/>
</Suspense>
</KibanaRenderContextProvider>
const mount = toMountPoint(
React.createElement(function () {
return (
<Suspense
fallback={
<EuiPortal>
<EuiProgress size="xs" position="fixed" />
</EuiPortal>
}
>
<NewVisModal
isOpen={true}
onClose={handleClose}
originatingApp={originatingApp}
stateTransfer={getEmbeddable().getStateTransfer()}
outsideVisualizeApp={outsideVisualizeApp}
editorParams={editorParams}
visTypesRegistry={getTypes()}
contentClient={getContentManagement().client}
uiSettings={getUISettings()}
addBasePath={getHttp().basePath.prepend}
application={getApplication()}
docLinks={getDocLinks()}
showAggsSelection={showAggsSelection}
selectedVisType={selectedVisType}
/>
</Suspense>
);
}),
{ analytics: getAnalytics(), i18n: getI18n(), theme: getTheme() }
);
ReactDOM.render(element, container);
unmount = mount(container);
return () => handleClose();
}

View file

@ -72,7 +72,8 @@
"@kbn/presentation-publishing",
"@kbn/shared-ux-markdown",
"@kbn/react-kibana-context-render",
"@kbn/react-kibana-mount"
"@kbn/react-kibana-mount",
"@kbn/presentation-containers"
],
"exclude": [
"target/**/*",

View file

@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should open editor menu when editor button is clicked', async () => {
await dashboardAddPanel.clickEditorMenuButton();
await testSubjects.existOrFail('dashboardEditorContextMenu');
await testSubjects.existOrFail('dashboardPanelSelectionFlyout');
});
});
}

View file

@ -52,16 +52,16 @@ export class DashboardAddPanelService extends FtrService {
async clickEditorMenuButton() {
this.log.debug('DashboardAddPanel.clickEditorMenuButton');
await this.testSubjects.click('dashboardEditorMenuButton');
await this.testSubjects.existOrFail('dashboardEditorContextMenu');
await this.testSubjects.existOrFail('dashboardPanelSelectionFlyout');
}
async expectEditorMenuClosed() {
await this.testSubjects.missingOrFail('dashboardEditorContextMenu');
await this.testSubjects.missingOrFail('dashboardPanelSelectionFlyout');
}
async clickAggBasedVisualizations() {
this.log.debug('DashboardAddPanel.clickEditorMenuAggBasedMenuItem');
await this.testSubjects.click('dashboardEditorAggBasedMenuItem');
await this.clickAddNewPanelFromUIActionLink('Aggregation based');
}
async clickVisType(visType: string) {
@ -69,9 +69,9 @@ export class DashboardAddPanelService extends FtrService {
await this.testSubjects.click(`visType-${visType}`);
}
async clickEmbeddableFactoryGroupButton(groupId: string) {
this.log.debug('DashboardAddPanel.clickEmbeddableFactoryGroupButton');
await this.testSubjects.click(`dashboardEditorMenu-${groupId}Group`);
async verifyEmbeddableFactoryGroupExists(groupId: string) {
this.log.debug('DashboardAddPanel.verifyEmbeddableFactoryGroupExists');
await this.testSubjects.existOrFail(`dashboardEditorMenu-${groupId}Group`);
}
async clickAddNewEmbeddableLink(type: string) {

View file

@ -34,11 +34,13 @@ export function createAddChangePointChartAction(
id: 'ml',
getDisplayName: () =>
i18n.translate('xpack.aiops.navMenu.mlAppNameText', {
defaultMessage: 'Machine Learning',
defaultMessage: 'Machine Learning and Analytics',
}),
getIconType: () => 'machineLearningApp',
},
],
order: 10,
getIconType: () => 'machineLearningApp',
getDisplayName: () =>
i18n.translate('xpack.aiops.embeddableChangePointChartDisplayName', {
defaultMessage: 'Change point detection',

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { type UiActionsSetup, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
import {
categorizeFieldTrigger,
@ -26,7 +26,7 @@ export function registerAiopsUiActions(
const openChangePointInMlAppAction = createOpenChangePointInMlAppAction(coreStart, pluginStart);
const addChangePointChartAction = createAddChangePointChartAction(coreStart, pluginStart);
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addChangePointChartAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addChangePointChartAction);
uiActions.registerTrigger(categorizeFieldTrigger);

View file

@ -158,14 +158,14 @@ export const EditorMenu: FC<Props> = ({
items: [
...visTypeAliases.map(getVisTypeAliasMenuItem),
...getAddPanelActionMenuItems(closePopover),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...promotedVisTypes.map(getVisTypeMenuItem),
...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
name: appName,
icon,
panel: panelId,
'data-test-subj': `canvasEditorMenu-${id}Group`,
})),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...promotedVisTypes.map(getVisTypeMenuItem),
],
},
...Object.values(factoryGroupMap).map(

View file

@ -7,7 +7,12 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { BaseVisType, VisGroups, VisTypeAlias } from '@kbn/visualizations-plugin/public';
import {
VisGroups,
type BaseVisType,
type VisTypeAlias,
type VisParams,
} from '@kbn/visualizations-plugin/public';
import {
EmbeddableFactory,
EmbeddableFactoryDefinition,
@ -201,13 +206,17 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
.map(({ factory }) => factory);
const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED);
const legacyVisTypes = getVisTypesByGroup(VisGroups.LEGACY);
return (
<Component
createNewVisType={createNewVisType}
createNewEmbeddableFromFactory={createNewEmbeddableFromFactory}
createNewEmbeddableFromAction={createNewEmbeddableFromAction}
promotedVisTypes={promotedVisTypes}
promotedVisTypes={([] as Array<BaseVisType<VisParams>>).concat(
promotedVisTypes,
legacyVisTypes
)}
factories={factories}
addPanelActions={addPanelActions}
visTypeAliases={visTypeAliases}

View file

@ -46,6 +46,7 @@ import {
ACTION_VISUALIZE_FIELD,
VISUALIZE_FIELD_TRIGGER,
VisualizeFieldContext,
ADD_PANEL_TRIGGER,
} from '@kbn/ui-actions-plugin/public';
import {
VISUALIZE_EDITOR_TRIGGER,
@ -648,7 +649,7 @@ export class LensPlugin {
// Displays the add ESQL panel in the dashboard add Panel menu
const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core);
startDependencies.uiActions.addTriggerAction('ADD_PANEL_TRIGGER', createESQLPanelAction);
startDependencies.uiActions.addTriggerAction(ADD_PANEL_TRIGGER, createESQLPanelAction);
const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR');
if (discoverLocator) {

View file

@ -9,6 +9,7 @@ import type { CoreStart } from '@kbn/core/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { COMMON_VISUALIZATION_GROUPING } from '@kbn/visualizations-plugin/public';
import type { LensPluginStartDependencies } from '../../plugin';
const ACTION_CREATE_ESQL_CHART = 'ACTION_CREATE_ESQL_CHART';
@ -20,6 +21,8 @@ export class CreateESQLPanelAction implements Action<EmbeddableApiContext> {
public id = ACTION_CREATE_ESQL_CHART;
public order = 50;
public grouping = COMMON_VISUALIZATION_GROUPING;
constructor(
protected readonly startDependencies: LensPluginStartDependencies,
protected readonly core: CoreStart

View file

@ -27,6 +27,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({
note: i18n.translate('xpack.lens.visTypeAlias.note', {
defaultMessage: 'Recommended for most users.',
}),
order: 60,
icon: 'lensApp',
stage: 'production',
appExtensions: {

View file

@ -33,6 +33,7 @@ export function getMapsVisTypeAlias() {
description: appDescription,
icon: APP_ICON,
stage: 'production' as VisualizationStage,
order: 40,
appExtensions: {
visualizations: {
docTypes: [MAP_SAVED_OBJECT_TYPE],

View file

@ -11,7 +11,7 @@ export const PLUGIN_ID = 'ml';
export const PLUGIN_ICON = 'machineLearningApp';
export const PLUGIN_ICON_SOLUTION = 'logoKibana';
export const ML_APP_NAME = i18n.translate('xpack.ml.navMenu.mlAppNameText', {
defaultMessage: 'Machine Learning',
defaultMessage: 'Machine Learning and Analytics',
});
export const ML_APP_ROUTE = '/app/ml';
export const ML_INTERNAL_BASE_PATH = '/internal/ml';

View file

@ -41,6 +41,10 @@ export function createAddAnomalyChartsPanelAction(
getIconType: () => PLUGIN_ICON,
},
],
order: 30,
getIconType(): string {
return 'visLine';
},
getDisplayName: () =>
i18n.translate('xpack.ml.components.mlAnomalyExplorerEmbeddable.displayName', {
defaultMessage: 'Anomaly chart',

View file

@ -42,6 +42,8 @@ export function createAddSingleMetricViewerPanelAction(
getIconType: () => PLUGIN_ICON,
},
],
order: 20,
getIconType: () => 'visLine',
getDisplayName: () =>
i18n.translate('xpack.ml.components.singleMetricViewerEmbeddable.displayName', {
defaultMessage: 'Single metric viewer',

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import type { PresentationContainer } from '@kbn/presentation-containers';
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
@ -41,6 +42,28 @@ export function createAddSwimlanePanelAction(
getIconType: () => PLUGIN_ICON,
},
],
order: 40,
// @ts-expect-error getIconType is typed as string, but EuiIcon accepts ReactComponent for custom icons.
// See https://github.com/elastic/kibana/issues/184643
getIconType: () => (iconProps) =>
(
<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
{...iconProps}
>
<path
d="M1 5V1H5V5H1ZM4 4V2H2V4H4ZM6 5V1H10V5H6ZM9 4V2H7V4H9ZM11 5V1H15V5H11ZM12 4H14V2H12V4ZM1 10V6H5V10H1ZM4 9V7H2V9H4ZM6 10V6H10V10H6ZM9 9V7H7V9H9ZM11 10V6H15V10H11ZM14 9V7H12V9H14ZM1 15V11H5V15H1ZM2 14H4V12H2V14ZM6 15V11H10V15H6ZM7 14H9V12H7V14ZM11 15V11H15V15H11ZM12 14H14V12H12V14Z"
fill="currentColor"
/>
<rect width="4" height="4" transform="translate(6 1)" fill="currentColor" />
<rect width="4" height="4" transform="translate(11 6)" fill="currentColor" />
<rect width="4" height="4" transform="translate(6 6)" fill="currentColor" />
<rect width="4" height="4" transform="translate(1 11)" fill="currentColor" />
</svg>
),
getDisplayName: () =>
i18n.translate('xpack.ml.components.jobAnomalyScoreEmbeddable.displayName', {
defaultMessage: 'Anomaly swim lane',

View file

@ -8,7 +8,7 @@
import type { CoreSetup } from '@kbn/core/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
import { CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER } from '@kbn/ml-ui-actions';
import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { type UiActionsSetup, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import type { MlPluginStart, MlStartDependencies } from '../plugin';
import { createApplyEntityFieldFiltersAction } from './apply_entity_filters_action';
import { createApplyInfluencerFiltersAction } from './apply_influencer_filters_action';
@ -67,9 +67,9 @@ export function registerMlUiActions(
uiActions.registerAction(addAnomalyChartsPanelAction);
// Assign triggers
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addSingleMetricViewerPanelAction);
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addSwimlanePanelAction);
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addAnomalyChartsPanelAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addSingleMetricViewerPanelAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addSwimlanePanelAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addAnomalyChartsPanelAction);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, openInExplorerAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, openInSingleMetricViewerAction.id);

View file

@ -21,7 +21,8 @@ import { BehaviorSubject, combineLatest, from } from 'rxjs';
import { map } from 'rxjs';
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
import { apiCanAddNewPanel } from '@kbn/presentation-containers';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public';
import type { InfraPublicConfig } from '../common/plugin_config_types';
import { createInventoryMetricRuleType } from './alerting/inventory';
import { createLogThresholdRuleType } from './alerting/log_threshold';
@ -400,6 +401,8 @@ export class Plugin implements InfraClientPluginClass {
plugins.uiActions.registerAction<EmbeddableApiContext>({
id: ADD_LOG_STREAM_ACTION_ID,
grouping: [COMMON_EMBEDDABLE_GROUPING.legacy],
order: 30,
getDisplayName: () =>
i18n.translate('xpack.infra.logStreamEmbeddable.displayName', {
defaultMessage: 'Log stream',
@ -427,7 +430,7 @@ export class Plugin implements InfraClientPluginClass {
);
},
});
plugins.uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_LOG_STREAM_ACTION_ID);
plugins.uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_LOG_STREAM_ACTION_ID);
const startContract: InfraClientStartExports = {
inventoryViews,

View file

@ -8,7 +8,7 @@
export const COMMON_SLO_GROUPING = [
{
id: 'slos',
getDisplayName: () => 'SLOs',
getDisplayName: () => 'Observability',
getIconType: () => {
return 'visGauge';
},

View file

@ -138,7 +138,8 @@ export class SloPlugin
const registerAsyncSloUiActions = async () => {
if (pluginsSetup.uiActions) {
const { registerSloUiActions } = await import('./ui_actions');
registerSloUiActions(pluginsSetup.uiActions, coreSetup);
registerSloUiActions(coreSetup, pluginsSetup, pluginsStart);
}
};
registerAsyncSloUiActions();

View file

@ -26,6 +26,7 @@ export function createAddAlertsPanelAction(
id: ADD_SLO_ALERTS_ACTION_ID,
grouping: COMMON_SLO_GROUPING,
getIconType: () => 'alert',
order: 20,
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);
},

View file

@ -24,6 +24,7 @@ export function createAddErrorBudgetPanelAction(
return {
id: ADD_SLO_ERROR_BUDGET_ACTION_ID,
grouping: COMMON_SLO_GROUPING,
order: 10,
getIconType: () => 'visLine',
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);

View file

@ -25,6 +25,7 @@ export function createOverviewPanelAction(
return {
id: ADD_SLO_OVERVIEW_ACTION_ID,
grouping: COMMON_SLO_GROUPING,
order: 30,
getIconType: () => 'visGauge',
isCompatible: async ({ embeddable }) => {
return apiIsPresentationContainer(embeddable);

View file

@ -5,24 +5,31 @@
* 2.0.
*/
import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
import type { CoreSetup } from '@kbn/core/public';
import { createOverviewPanelAction } from './create_overview_panel_action';
import { createAddErrorBudgetPanelAction } from './create_error_budget_action';
import { createAddAlertsPanelAction } from './create_alerts_panel_action';
import { SloPublicPluginsStart, SloPublicStart } from '..';
import { SloPublicPluginsStart, SloPublicStart, SloPublicPluginsSetup } from '..';
export function registerSloUiActions(
uiActions: UiActionsSetup,
core: CoreSetup<SloPublicPluginsStart, SloPublicStart>
core: CoreSetup<SloPublicPluginsStart, SloPublicStart>,
pluginsSetup: SloPublicPluginsSetup,
pluginsStart: SloPublicPluginsStart
) {
const { uiActions } = pluginsSetup;
const { serverless, cloud } = pluginsStart;
// Initialize actions
const addOverviewPanelAction = createOverviewPanelAction(core.getStartServices);
const addErrorBudgetPanelAction = createAddErrorBudgetPanelAction(core.getStartServices);
const addAlertsPanelAction = createAddAlertsPanelAction(core.getStartServices);
// Assign triggers
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addOverviewPanelAction);
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addErrorBudgetPanelAction);
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addAlertsPanelAction);
// Only register these actions in stateful kibana, and the serverless observability project
if (Boolean((serverless && cloud?.serverless.projectType === 'observability') || !serverless)) {
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addOverviewPanelAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addErrorBudgetPanelAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addAlertsPanelAction);
}
}

View file

@ -1192,8 +1192,6 @@
"dashboard.actions.downloadOptionsUnsavedFilename": "sans titre",
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "Minimiser",
"dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "Maximiser le panneau",
"dashboard.addPanelMenuTrigger.description": "Une nouvelle action apparaîtra dans le menu Ajouter un panneau du tableau de bord",
"dashboard.addPanelMenuTrigger.title": "Menu Ajouter un panneau",
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "Annuler",
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "Quitter Dashboard sans enregistrer ?",
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées",
@ -1215,7 +1213,6 @@
"dashboard.editingToolbar.controlsButtonTitle": "Contrôles",
"dashboard.editingToolbar.editControlGroupButtonTitle": "Paramètres",
"dashboard.editingToolbar.onlyOneTimeSliderControlMsg": "Le groupe de contrôle contient déjà un contrôle de curseur temporel.",
"dashboard.editorMenu.aggBasedGroupTitle": "Basé sur une agrégation",
"dashboard.editorMenu.deprecatedTag": "Déclassé",
"dashboard.embeddableApi.showSettings.flyout.applyButtonTitle": "Appliquer",
"dashboard.embeddableApi.showSettings.flyout.cancelButtonTitle": "Annuler",
@ -44235,6 +44232,8 @@
"uiActions.errors.incompatibleAction": "Action non compatible",
"uiActions.triggers.rowClickkDescription": "Un clic sur une ligne de tableau",
"uiActions.triggers.rowClickTitle": "Clic sur ligne de tableau",
"uiActions.triggers.dashboard.addPanelMenu.description": "Une nouvelle action apparaîtra dans le menu Ajouter un panneau du tableau de bord",
"uiActions.triggers.dashboard.addPanelMenu.title": "Menu Ajouter un panneau",
"unsavedChangesBadge.contextMenu.openButton": "Afficher les actions disponibles",
"unsavedChangesBadge.contextMenu.revertChangesButton": "Restaurer les modifications",
"unsavedChangesBadge.contextMenu.revertingChangesButtonStatus": "Annuler les modifications",

View file

@ -1192,8 +1192,6 @@
"dashboard.actions.downloadOptionsUnsavedFilename": "無題",
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
"dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "パネルを最大化",
"dashboard.addPanelMenuTrigger.description": "新しいアクションは、ダッシュボードのパネルの追加メニューに表示されます",
"dashboard.addPanelMenuTrigger.title": "パネルの追加メニュー",
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "キャンセル",
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "作業を保存せずにダッシュボードから移動しますか?",
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "保存されていない変更",
@ -1215,7 +1213,6 @@
"dashboard.editingToolbar.controlsButtonTitle": "コントロール",
"dashboard.editingToolbar.editControlGroupButtonTitle": "設定",
"dashboard.editingToolbar.onlyOneTimeSliderControlMsg": "コントロールグループには、すでに時間スライダーコントロールがあります。",
"dashboard.editorMenu.aggBasedGroupTitle": "アグリゲーションに基づく",
"dashboard.editorMenu.deprecatedTag": "非推奨",
"dashboard.embeddableApi.showSettings.flyout.applyButtonTitle": "適用",
"dashboard.embeddableApi.showSettings.flyout.cancelButtonTitle": "キャンセル",
@ -44209,6 +44206,8 @@
"uiActions.actionPanel.more": "詳細",
"uiActions.actionPanel.title": "オプション",
"uiActions.errors.incompatibleAction": "操作に互換性がありません",
"uiActions.triggers.dashboard.addPanelMenu.description": "新しいアクションは、ダッシュボードのパネルの追加メニューに表示されます",
"uiActions.triggers.dashboard.addPanelMenu.title": "パネルの追加メニュー",
"uiActions.triggers.rowClickkDescription": "テーブル行をクリック",
"uiActions.triggers.rowClickTitle": "テーブル行クリック",
"unsavedChangesBadge.contextMenu.openButton": "使用可能なアクションを表示",

View file

@ -1194,8 +1194,6 @@
"dashboard.actions.downloadOptionsUnsavedFilename": "未命名",
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
"dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "最大化面板",
"dashboard.addPanelMenuTrigger.description": "一个新操作将在仪表板的添加面板菜单中显示出来",
"dashboard.addPanelMenuTrigger.title": "添加面板菜单",
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "取消",
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "离开有未保存工作的仪表板?",
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "未保存的更改",
@ -1217,7 +1215,6 @@
"dashboard.editingToolbar.controlsButtonTitle": "控件",
"dashboard.editingToolbar.editControlGroupButtonTitle": "设置",
"dashboard.editingToolbar.onlyOneTimeSliderControlMsg": "控件组已包含时间滑块控件。",
"dashboard.editorMenu.aggBasedGroupTitle": "基于聚合",
"dashboard.editorMenu.deprecatedTag": "(已过时)",
"dashboard.embeddableApi.showSettings.flyout.applyButtonTitle": "应用",
"dashboard.embeddableApi.showSettings.flyout.cancelButtonTitle": "取消",
@ -44257,6 +44254,8 @@
"uiActions.actionPanel.more": "更多",
"uiActions.actionPanel.title": "选项",
"uiActions.errors.incompatibleAction": "操作不兼容",
"uiActions.triggers.dashboard.addPanelMenu.description": "一个新操作将在仪表板的添加面板菜单中显示出来",
"uiActions.triggers.dashboard.addPanelMenu.title": "添加面板菜单",
"uiActions.triggers.rowClickkDescription": "表格行的单击",
"uiActions.triggers.rowClickTitle": "表格行单击",
"unsavedChangesBadge.contextMenu.openButton": "查看可用操作",

View file

@ -33,6 +33,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.dashboard.switchToEditMode();
}
await dashboardAddPanel.clickEditorMenuButton();
await testSubjects.setValue('dashboardPanelSelectionFlyout__searchInput', 'maps');
await dashboardAddPanel.clickVisType('maps');
await PageObjects.maps.clickSaveAndReturnButton();
}

View file

@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('Single SLO', function () {
it('should open SLO configuration flyout', async () => {
await dashboardAddPanel.clickEditorMenuButton();
await dashboardAddPanel.clickEmbeddableFactoryGroupButton('slos');
await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('slos');
await dashboardAddPanel.clickAddNewPanelFromUIActionLink('SLO Overview');
await sloUi.common.assertSloOverviewConfigurationExists();
});
@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('Group of SLOs', function () {
it('can select Group Overview mode in the Flyout configuration', async () => {
await dashboardAddPanel.clickEditorMenuButton();
await dashboardAddPanel.clickEmbeddableFactoryGroupButton('slos');
await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('slos');
await dashboardAddPanel.clickAddNewPanelFromUIActionLink('SLO Overview');
await sloUi.common.clickOverviewMode();
await sloUi.common.assertSloConfigurationGroupOverviewModeIsSelected();

View file

@ -124,9 +124,9 @@ export function MachineLearningDashboardEmbeddablesProvider(
};
await retry.tryForTime(60 * 1000, async () => {
await dashboardAddPanel.clickEditorMenuButton();
await testSubjects.existOrFail('dashboardEditorContextMenu', { timeout: 2000 });
await testSubjects.existOrFail('dashboardPanelSelectionFlyout', { timeout: 2000 });
await dashboardAddPanel.clickEmbeddableFactoryGroupButton('ml');
await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('ml');
await dashboardAddPanel.clickAddNewPanelFromUIActionLink(name[mlEmbeddableType]);
await testSubjects.existOrFail('mlAnomalyJobSelectionControls', { timeout: 2000 });