From 22e0545d0e89aa39c39f3f6a57467aa16f165679 Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:55:28 +0200 Subject: [PATCH] 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 Screenshot 2024-06-10 at 17 44 17 #### Search match view Screenshot 2024-06-10 at 17 45 11 ##### 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) - [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) --------- Co-authored-by: Catherine Liu Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../data_table/create_data_table_action.ts | 8 +- .../embeddable_examples_grouping.ts | 1 + .../create_eui_markdown_action.tsx | 8 +- .../field_list/create_field_list_action.tsx | 4 +- .../saved_book/create_saved_book_action.tsx | 4 +- .../register_add_search_panel_action.tsx | 8 +- .../triggers/dashboard_app_panel_trigger.ts | 8 +- .../src/triggers/index.ts | 1 + .../public/dashboard_actions/index.ts | 1 - .../add_panel_action_menu_items.test.ts | 50 ++- .../top_nav/add_panel_action_menu_items.ts | 80 ++-- .../dashboard_app/top_nav/editor_menu.scss | 2 +- .../top_nav/editor_menu.test.tsx | 75 ++-- .../dashboard_app/top_nav/editor_menu.tsx | 369 ++++++++---------- .../open_dashboard_panel_selection_flyout.tsx | 255 ++++++++++++ src/plugins/dashboard/public/plugin.tsx | 7 +- src/plugins/embeddable/public/index.ts | 2 + .../lib/embeddables/common/constants.ts | 37 ++ .../lib/embeddables/embeddable_factory.ts | 2 + .../public/actions/create_image_action.ts | 7 +- .../public/embeddable/links_embeddable.tsx | 3 + .../embeddable/links_embeddable_factory.ts | 4 +- src/plugins/ui_actions/public/index.ts | 3 + src/plugins/ui_actions/public/plugin.ts | 2 + .../vis_type_markdown/public/markdown_vis.ts | 1 + .../timeseries/public/metrics_type.ts | 3 +- .../public/actions/add_agg_vis_action.ts | 64 +++ .../public/embeddable/constants.ts | 11 + .../visualizations/public/embeddable/index.ts | 2 +- src/plugins/visualizations/public/index.ts | 1 + src/plugins/visualizations/public/plugin.ts | 13 +- .../public/vis_types/base_vis_type.ts | 2 + .../visualizations/public/vis_types/types.ts | 2 + .../public/vis_types/vis_groups_enum.ts | 1 + .../vis_types/vis_type_alias_registry.ts | 1 + .../agg_based_selection.tsx | 9 +- .../group_selection/group_selection.tsx | 3 + .../public/wizard/new_vis_modal.tsx | 1 + .../public/wizard/show_new_vis.tsx | 78 ++-- src/plugins/visualizations/tsconfig.json | 3 +- .../apps/dashboard/group5/empty_dashboard.ts | 2 +- .../services/dashboard/add_panel.ts | 12 +- .../ui_actions/create_change_point_chart.tsx | 4 +- .../plugins/aiops/public/ui_actions/index.ts | 4 +- .../editor_menu/editor_menu.component.tsx | 4 +- .../editor_menu/editor_menu.tsx | 13 +- x-pack/plugins/lens/public/plugin.ts | 3 +- .../open_lens_config/create_action.tsx | 3 + x-pack/plugins/lens/public/vis_type_alias.ts | 1 + .../maps/public/maps_vis_type_alias.ts | 1 + x-pack/plugins/ml/common/constants/app.ts | 2 +- .../ui_actions/create_anomaly_chart.tsx | 4 + .../create_single_metric_viewer.tsx | 2 + .../ml/public/ui_actions/create_swim_lane.tsx | 23 ++ x-pack/plugins/ml/public/ui_actions/index.ts | 8 +- .../infra/public/plugin.ts | 7 +- .../public/embeddable/slo/common/constants.ts | 2 +- .../slo/public/plugin.ts | 3 +- .../ui_actions/create_alerts_panel_action.tsx | 1 + .../ui_actions/create_error_budget_action.tsx | 1 + .../create_overview_panel_action.tsx | 1 + .../slo/public/ui_actions/index.ts | 21 +- .../translations/translations/fr-FR.json | 5 +- .../translations/translations/ja-JP.json | 5 +- .../translations/translations/zh-CN.json | 5 +- .../group2/dashboard_maps_by_value.ts | 1 + .../slo/embeddables/overview_embeddable.ts | 4 +- .../services/ml/dashboard_embeddables.ts | 4 +- 68 files changed, 886 insertions(+), 396 deletions(-) rename src/plugins/dashboard/public/triggers/index.ts => packages/kbn-ui-actions-browser/src/triggers/dashboard_app_panel_trigger.ts (76%) create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx create mode 100644 src/plugins/embeddable/public/lib/embeddables/common/constants.ts create mode 100644 src/plugins/visualizations/public/actions/add_agg_vis_action.ts diff --git a/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts b/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts index 9971535e148dd..6364073ade676 100644 --- a/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts +++ b/examples/embeddable_examples/public/react_embeddables/data_table/create_data_table_action.ts @@ -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); }; diff --git a/examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts b/examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts index fa2ecd03b5d25..4c7f52d261fcb 100644 --- a/examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts +++ b/examples/embeddable_examples/public/react_embeddables/embeddable_examples_grouping.ts @@ -10,4 +10,5 @@ export const embeddableExamplesGrouping = { id: 'embeddableExamples', getIconType: () => 'documentation', getDisplayName: () => 'Embeddable examples', + order: -10, }; diff --git a/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx b/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx index 81c23a4d960b8..c5eb2d72cc479 100644 --- a/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/eui_markdown/create_eui_markdown_action.tsx @@ -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. diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx index e05868e7737d1..175c3119955a2 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/field_list/create_field_list_action.tsx @@ -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); }; diff --git a/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx b/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx index 6916bd38cc28d..eaaa607f76001 100644 --- a/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx @@ -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); }; diff --git a/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx b/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx index 945e969187631..1bffd091164b0 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/search/register_add_search_panel_action.tsx @@ -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. diff --git a/src/plugins/dashboard/public/triggers/index.ts b/packages/kbn-ui-actions-browser/src/triggers/dashboard_app_panel_trigger.ts similarity index 76% rename from src/plugins/dashboard/public/triggers/index.ts rename to packages/kbn-ui-actions-browser/src/triggers/dashboard_app_panel_trigger.ts index 96dfa814b949a..74e894d2b5563 100644 --- a/src/plugins/dashboard/public/triggers/index.ts +++ b/packages/kbn-ui-actions-browser/src/triggers/dashboard_app_panel_trigger.ts @@ -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", }), }; diff --git a/packages/kbn-ui-actions-browser/src/triggers/index.ts b/packages/kbn-ui-actions-browser/src/triggers/index.ts index 091305791d858..d298be1524411 100644 --- a/packages/kbn-ui-actions-browser/src/triggers/index.ts +++ b/packages/kbn-ui-actions-browser/src/triggers/index.ts @@ -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'; diff --git a/src/plugins/dashboard/public/dashboard_actions/index.ts b/src/plugins/dashboard/public/dashboard_actions/index.ts index ecffeaa5643ae..ba93db83247d4 100644 --- a/src/plugins/dashboard/public/dashboard_actions/index.ts +++ b/src/plugins/dashboard/public/dashboard_actions/index.ts @@ -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'; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts index ed2fadd359db7..180b9b7eb2ff9 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts @@ -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), + 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), - toolTipContent: 'Action tooltip', + '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({}); }); }); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts index 678129d0f5808..4e90d94caa388 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts @@ -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 { + 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, 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> | undefined, closePopover: () => void -): [EuiContextMenuPanelItemDescriptor[], Record] => { - const ungrouped: EuiContextMenuPanelItemDescriptor[] = []; +) => { const grouped: Record = {}; const context = { @@ -47,29 +64,31 @@ export const getAddPanelActionMenuItems = ( trigger: addPanelMenuTrigger, }; - const getMenuItem = (item: Action) => { + const getMenuItem = (item: Action): 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; }; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss index 859a0ee11067e..0f463926908f3 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss @@ -3,4 +3,4 @@ @include euiOverflowShadow; max-height: 60vh; overflow-y: scroll; -} \ No newline at end of file +} diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx index 6b2ff3f1900a3..c2b51a5e0eda5 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx @@ -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; @@ -46,29 +47,23 @@ describe('mergeGroupedItemsProvider', () => { id: 'panel2', title: 'Panel 2', icon: 'icon2', + order: 10, items: [ { id: 'addPanelActionId', + order: 0, }, ], }, } as unknown as Record; 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', }, ]); }); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index d92b913c28b5d..ba7811dfec360 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -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; 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 = (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, groupedAddPanelAction: Record - ): [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>(); + 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>(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 = {}; - 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[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 - ) : ( - - {titleInWizard || title} - - - {i18n.translate('dashboard.editorMenu.deprecatedTag', { - defaultMessage: 'Deprecated', - })} - - - - ), - 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 = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { - const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closePopover); + const getEditorMenuPanels = (closeFlyout: () => void): GroupedAddPanelActions[] => { + const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closeFlyout); - const [ungroupedAddPanelActions, groupedAddPanelAction] = getAddPanelActionMenuItems( + const groupedAddPanelAction = getAddPanelActionMenuItemsGroup( api, addPanelActions, - closePopover + closeFlyout ); - const [initialPanelGroups, additionalPanels] = 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`, - }); - } + const initialPanelGroups = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( + factoryGroupMap, + groupedAddPanelAction + ); - return [ - { - id: 0, - items: initialPanelItems, - }, - { - id: aggBasedPanelID, - title: aggsPanelTitle, - items: aggsBasedVisTypes.map(getVisTypeMenuItem), - }, - ...additionalPanels, - ]; + // enhance panel groups + return sortGroupPanelsByOrder(initialPanelGroups).map((panelGroup) => { + switch (panelGroup.id) { + case 'visualizations': { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (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( + (panelGroup.items ?? []).concat( + legacyVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) + ) + ), + }; + } + case COMMON_EMBEDDABLE_GROUPING.annotation.id: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + toolVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) + ) + ), + }; + } + default: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder(panelGroup.items), + }; + } + } + }); }; return ( - { + flyoutRef.current = openDashboardPanelSelectionFlyout({ + getPanels: getEditorMenuPanels, + }); + }} size="s" - iconType="plusInCircle" - panelPaddingSize="none" - data-test-subj="dashboardEditorMenuButton" - > - {({ closePopover }: { closePopover: () => void }) => ( - - )} - + /> ); }; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx new file mode 100644 index 0000000000000..8bd8dffc67c97 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx @@ -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; +} + +interface Props extends Pick { + /** Handler to close flyout */ + close: () => void; + /** Padding for flyout */ + paddingSize: Exclude; +} + +export function openDashboardPanelSelectionFlyout({ + getPanels, + flyoutPanelPaddingSize = 'l', +}: OpenDashboardPanelSelectionFlyoutArgs) { + const { + overlays, + analytics, + settings: { i18n, theme }, + } = pluginServices.getServices(); + // eslint-disable-next-line prefer-const + let flyoutRef: ReturnType; + + const mount = toMountPoint( + React.createElement(function () { + const closeFlyout = () => flyoutRef.close(); + return ( + + ); + }), + { 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 = ({ + close, + getPanels, + paddingSize, +}) => { + const { euiTheme } = useEuiTheme(); + const panels = useRef(getPanels(close)); + const [searchTerm, setSearchTerm] = useState(''); + const [panelsSearchResult, setPanelsSearchResult] = useState( + 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 ( + <> + + +

+ +

+
+
+ + + + + + { + 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" + /> + + + + + + {panelsSearchResult.some(({ isDisabled }) => !isDisabled) ? ( + panelsSearchResult.map( + ({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj }) => + !isDisabled ? ( + + + {typeof title === 'string' ?

{title}

: title} +
+ + {items?.map((item, idx) => { + return ( + + {!item.isDeprecated ? ( + {item.name} + ) : ( + + + {item.name} + + + + + + + + )} + + } + onClick={item?.onClick} + iconType={item.icon} + data-test-subj={item['data-test-subj']} + isDisabled={item.isDisabled} + /> + ); + })} + +
+ ) : null + ) + ) : ( + + + + )} +
+
+
+
+ + + + + + + + + + + ); +}; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 0a231492d70b9..c2838187d5eca 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -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(); - // 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: {}, diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index b00adeb711a5b..88faa59b51c81 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -108,3 +108,5 @@ export { embeddableInputToSubject, embeddableOutputToSubject, } from './lib/embeddables/compatibility/embeddable_compatibility_utils'; + +export { COMMON_EMBEDDABLE_GROUPING } from './lib/embeddables/common/constants'; diff --git a/src/plugins/embeddable/public/lib/embeddables/common/constants.ts b/src/plugins/embeddable/public/lib/embeddables/common/constants.ts new file mode 100644 index 0000000000000..78228ec370a0e --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/common/constants.ts @@ -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 } = { + 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, + }, +}; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index cb19b82d75c98..39e32f756ae0c 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -148,4 +148,6 @@ export interface EmbeddableFactory< initialInput: TEmbeddableInput, parent?: IContainer ): Promise; + + order?: number; } diff --git a/src/plugins/image_embeddable/public/actions/create_image_action.ts b/src/plugins/image_embeddable/public/actions/create_image_action.ts index 02cd8b26e1182..bbbe0144856e0 100644 --- a/src/plugins/image_embeddable/public/actions/create_image_action.ts +++ b/src/plugins/image_embeddable/public/actions/create_image_action.ts @@ -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({ 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. diff --git a/src/plugins/links/public/embeddable/links_embeddable.tsx b/src/plugins/links/public/embeddable/links_embeddable.tsx index dcc49a7265a43..523d8706b2b86 100644 --- a/src/plugins/links/public/embeddable/links_embeddable.tsx +++ b/src/plugins/links/public/embeddable/links_embeddable.tsx @@ -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(); + public grouping = [COMMON_EMBEDDABLE_GROUPING.annotation]; + constructor( config: LinksConfig, initialInput: LinksInput, diff --git a/src/plugins/links/public/embeddable/links_embeddable_factory.ts b/src/plugins/links/public/embeddable/links_embeddable_factory.ts index 9ff3877b8a42e..40d377345e4f2 100644 --- a/src/plugins/links/public/embeddable/links_embeddable_factory.ts +++ b/src/plugins/links/public/embeddable/links_embeddable_factory.ts @@ -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) => Record) | undefined; migrations?: MigrateFunctionsObject | GetMigrationFunctionObjectFn | undefined; - grouping?: UiActionsPresentableGrouping | undefined; + grouping: UiActionsPresentableGrouping = [COMMON_EMBEDDABLE_GROUPING.annotation]; + public readonly type = CONTENT_ID; public readonly isContainerType = false; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 059ebad4b2bed..cb9a2ae53ef03 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -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 { diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index 1dbff5b9729a0..5da343a0f6400 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -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); diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index 33acfa21cd0b0..4ebda6058777d 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -27,6 +27,7 @@ export const markdownVisDefinition: VisTypeDefinition = { description: i18n.translate('visTypeMarkdown.markdownDescription', { defaultMessage: 'Add text and images to your dashboard.', }), + order: 30, toExpressionAst, visConfig: { defaults: { diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 51b0fcce5a58d..92361c6c53e19 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -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(), diff --git a/src/plugins/visualizations/public/actions/add_agg_vis_action.ts b/src/plugins/visualizations/public/actions/add_agg_vis_action.ts new file mode 100644 index 0000000000000..62c8e3654db6e --- /dev/null +++ b/src/plugins/visualizations/public/actions/add_agg_vis_action.ts @@ -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 { + 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 { + if (!isApiCompatible(embeddable)) { + throw new IncompatibleActionError(); + } + + showNewVisModal({ + originatingApp: embeddable.getAppContext().currentAppId, + outsideVisualizeApp: true, + showAggsSelection: true, + }); + } +} diff --git a/src/plugins/visualizations/public/embeddable/constants.ts b/src/plugins/visualizations/public/embeddable/constants.ts index cec3bd6cdfc88..920562103e242 100644 --- a/src/plugins/visualizations/public/embeddable/constants.ts +++ b/src/plugins/visualizations/public/embeddable/constants.ts @@ -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, + }, +]; diff --git a/src/plugins/visualizations/public/embeddable/index.ts b/src/plugins/visualizations/public/embeddable/index.ts index ae0748f4475c2..ed3fef1c9ad44 100644 --- a/src/plugins/visualizations/public/embeddable/index.ts +++ b/src/plugins/visualizations/public/embeddable/index.ts @@ -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'; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 2f00bba142009..838ac3dbd7547 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -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'; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index bc208c6bb785a..c97ff8f4eba45 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -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); diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 26801421159b3..7a412aa40f02e 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -24,6 +24,7 @@ const defaultOptions: VisTypeOptions = { export class BaseVisType { 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 { 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: {} }); diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 2f689cb81aee0..b1920d5bb3a60 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -217,4 +217,6 @@ export interface VisTypeDefinition { * have incosistencies in legacy visLib visualizations */ readonly visConfig: Record; + + readonly order?: number; } diff --git a/src/plugins/visualizations/public/vis_types/vis_groups_enum.ts b/src/plugins/visualizations/public/vis_types/vis_groups_enum.ts index c8bd320c2f61b..10a38cb69ba1b 100644 --- a/src/plugins/visualizations/public/vis_types/vis_groups_enum.ts +++ b/src/plugins/visualizations/public/vis_types/vis_groups_enum.ts @@ -10,4 +10,5 @@ export enum VisGroups { PROMOTED = 'promoted', TOOLS = 'tools', AGGBASED = 'aggbased', + LEGACY = 'legacy', } diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 617f0386f6181..f736455faf046 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -117,6 +117,7 @@ export interface VisTypeAlias { visualizations: VisualizationsAppExtension; [appName: string]: unknown; }; + order?: number; } let registry: VisTypeAlias[] = []; diff --git a/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx b/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx index f4cdd05978830..1cf6ced52f412 100644 --- a/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx +++ b/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx @@ -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 - this.props.toggleGroups(true)} /> + {this.props.openedAsRoot ? null : ( + this.props.toggleGroups(true)} /> + )} { return !visDefinition.disableCreate; }), @@ -65,6 +67,7 @@ function GroupSelection(props: GroupSelectionProps) { ), [props.visTypesRegistry] ); + return ( <> diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index b1e5de3215260..382474dd11b50 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -106,6 +106,7 @@ class NewVisModal extends React.Component this.setState({ showGroups: flag })} + openedAsRoot={this.props.showAggsSelection && !this.props.selectedVisType} /> ); diff --git a/src/plugins/visualizations/public/wizard/show_new_vis.tsx b/src/plugins/visualizations/public/wizard/show_new_vis.tsx index 867af06637ce0..e63ddc48f00df 100644 --- a/src/plugins/visualizations/public/wizard/show_new_vis.tsx +++ b/src/plugins/visualizations/public/wizard/show_new_vis.tsx @@ -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>; + 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 = ( - - - - - } - > - - - + const mount = toMountPoint( + React.createElement(function () { + return ( + + + + } + > + + + ); + }), + { analytics: getAnalytics(), i18n: getI18n(), theme: getTheme() } ); - ReactDOM.render(element, container); + + unmount = mount(container); return () => handleClose(); } diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 56d8275f80eaa..1592eff839af3 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -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/**/*", diff --git a/test/functional/apps/dashboard/group5/empty_dashboard.ts b/test/functional/apps/dashboard/group5/empty_dashboard.ts index 6939833a80086..a26382daa91a7 100644 --- a/test/functional/apps/dashboard/group5/empty_dashboard.ts +++ b/test/functional/apps/dashboard/group5/empty_dashboard.ts @@ -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'); }); }); } diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index 1f4fa6b14aeb9..ffc62bdfdb68a 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -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) { diff --git a/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx b/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx index f3bd3c359a342..16ad06b103927 100644 --- a/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx +++ b/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx @@ -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', diff --git a/x-pack/plugins/aiops/public/ui_actions/index.ts b/x-pack/plugins/aiops/public/ui_actions/index.ts index daa1b8ffd5ff8..c8e4edb58792b 100644 --- a/x-pack/plugins/aiops/public/ui_actions/index.ts +++ b/x-pack/plugins/aiops/public/ui_actions/index.ts @@ -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); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx index 24dcde46cf902..5c424961d7f50 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx @@ -158,14 +158,14 @@ export const EditorMenu: FC = ({ 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( diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx index 4e50d56f3cb77..fd644903ac25d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -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 = ({ addElement }) => { .map(({ factory }) => factory); const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED); + const legacyVisTypes = getVisTypesByGroup(VisGroups.LEGACY); return ( >).concat( + promotedVisTypes, + legacyVisTypes + )} factories={factories} addPanelActions={addPanelActions} visTypeAliases={visTypeAliases} diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index cd48b0b33782e..ba615678c334e 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -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) { diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx index 07301f2394130..f1d58f9702fb4 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx @@ -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 { public id = ACTION_CREATE_ESQL_CHART; public order = 50; + public grouping = COMMON_VISUALIZATION_GROUPING; + constructor( protected readonly startDependencies: LensPluginStartDependencies, protected readonly core: CoreStart diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index e20b60a11c57a..90d1df663d3e4 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -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: { diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index 48a96a2d0f988..dc7c8cc8dc7bd 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -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], diff --git a/x-pack/plugins/ml/common/constants/app.ts b/x-pack/plugins/ml/common/constants/app.ts index d74e1cb30c761..dd41353184fd4 100644 --- a/x-pack/plugins/ml/common/constants/app.ts +++ b/x-pack/plugins/ml/common/constants/app.ts @@ -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'; diff --git a/x-pack/plugins/ml/public/ui_actions/create_anomaly_chart.tsx b/x-pack/plugins/ml/public/ui_actions/create_anomaly_chart.tsx index c4b88bdd43306..28b7b413827a5 100644 --- a/x-pack/plugins/ml/public/ui_actions/create_anomaly_chart.tsx +++ b/x-pack/plugins/ml/public/ui_actions/create_anomaly_chart.tsx @@ -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', diff --git a/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx b/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx index 55b3bdf44663b..3b1e06f62538a 100644 --- a/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx +++ b/x-pack/plugins/ml/public/ui_actions/create_single_metric_viewer.tsx @@ -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', diff --git a/x-pack/plugins/ml/public/ui_actions/create_swim_lane.tsx b/x-pack/plugins/ml/public/ui_actions/create_swim_lane.tsx index 388b32148847f..b0f47eb4e1dc7 100644 --- a/x-pack/plugins/ml/public/ui_actions/create_swim_lane.tsx +++ b/x-pack/plugins/ml/public/ui_actions/create_swim_lane.tsx @@ -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) => + ( + + + + + + + + ), getDisplayName: () => i18n.translate('xpack.ml.components.jobAnomalyScoreEmbeddable.displayName', { defaultMessage: 'Anomaly swim lane', diff --git a/x-pack/plugins/ml/public/ui_actions/index.ts b/x-pack/plugins/ml/public/ui_actions/index.ts index 66dd1f0f06f34..1b650d331d007 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -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); diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index 20f819b7eb34d..f323d7adb6297 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -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({ 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, diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/constants.ts b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/constants.ts index 94cf6dd80fa5e..b263c043d7acf 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/constants.ts +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/constants.ts @@ -8,7 +8,7 @@ export const COMMON_SLO_GROUPING = [ { id: 'slos', - getDisplayName: () => 'SLOs', + getDisplayName: () => 'Observability', getIconType: () => { return 'visGauge'; }, diff --git a/x-pack/plugins/observability_solution/slo/public/plugin.ts b/x-pack/plugins/observability_solution/slo/public/plugin.ts index 5bdd830830fd6..5748a55c21489 100644 --- a/x-pack/plugins/observability_solution/slo/public/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/public/plugin.ts @@ -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(); diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx index b365881bf915a..63926aa24cd82 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_alerts_panel_action.tsx @@ -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); }, diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx index c619252bc6eb8..8d311bfdce70b 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_error_budget_action.tsx @@ -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); diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx index edb7b129927b0..49d2d269d4cdd 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/create_overview_panel_action.tsx @@ -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); diff --git a/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts b/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts index 61c1569f1a9d7..95c1f19a8842a 100644 --- a/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts +++ b/x-pack/plugins/observability_solution/slo/public/ui_actions/index.ts @@ -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 + core: CoreSetup, + 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); + } } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 1e2d0b57533ff..ae1c41e947088 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 305baf56ccc5c..9635294d16161 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "使用可能なアクションを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 49bc0b78a8638..f2e659ca333bc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "查看可用操作", diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts index 61fc8319cb907..6cfdc7356a4b5 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts @@ -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(); } diff --git a/x-pack/test/functional/apps/slo/embeddables/overview_embeddable.ts b/x-pack/test/functional/apps/slo/embeddables/overview_embeddable.ts index a9671b6e350d6..5a531d6cf5bb5 100644 --- a/x-pack/test/functional/apps/slo/embeddables/overview_embeddable.ts +++ b/x-pack/test/functional/apps/slo/embeddables/overview_embeddable.ts @@ -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(); diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index 9a5428276479e..b22622ead61d0 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -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 });